diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dcd9f9c14..291812c21 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,9 +4,11 @@ 1. Clone the repository locally 2. If you want to change something in DevPod agent code: - 1. Exchange the URL in [DefaultAgentDownloadURL](./pkg/agent/agent.go) with a custom public repository release you have created. + 1. Exchange the URL in [DefaultAgentDownloadURL](./pkg/agent/agent.go) with a + custom public repository release you have created. 2. Build devpod via: `./hack/rebuild.sh` - 3. Upload `test/devpod-linux-amd64` and `test/devpod-linux-arm64` to the public repository release assets. + 3. Upload `test/devpod-linux-amd64` and `test/devpod-linux-arm64` to the public + repository release assets. 3. Build devpod via: `./hack/rebuild.sh` (asking for sudo password) 4. Add docker provider via `devpod provider add docker` 5. Configure docker provider via `devpod use provider docker` @@ -15,24 +17,30 @@ ## Build from source Prerequisites CLI: + - [Go 1.20](https://go.dev/doc/install) -Once installed, run +Once installed, run `CGO_ENABLED=0 go build -ldflags "-s -w" -o devpod-cli` Prerequisites GUI: + - [NodeJS + yarn](https://nodejs.org/en/) - [Rust](https://www.rust-lang.org/tools/install) - [Go](https://go.dev/doc/install) To build the app on Linux, you will need the following dependencies: -```console -sudo apt-get install libappindicator3-1 libgdk-pixbuf2.0-0 libbsd0 libxdmcp6 libwmf-0.2-7 libwmf-0.2-7-gtk libgtk-3-0 libwmf-dev libwebkit2gtk-4.0-37 librust-openssl-sys-dev librust-glib-sys-dev - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev +```bash +sudo apt-get install libappindicator3-1 libgdk-pixbuf2.0-0 libbsd0 libxdmcp6 \ + libwmf-0.2-7 libwmf-0.2-7-gtk libgtk-3-0 libwmf-dev libwebkit2gtk-4.0-37 \ + librust-openssl-sys-dev librust-glib-sys-dev +sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev \ + libayatana-appindicator3-dev librsvg2-dev ``` Once installed, run + - `cd desktop` - `yarn tauri build --config src-tauri/tauri-dev.conf.json` @@ -40,7 +48,8 @@ The application should be in `desktop/src-tauri/target/release` ## Provider -Head over to [the docs](https://devpod.sh/docs/developing-providers/quickstart) for an introduction into developing your own providers +Head over to [the docs](https://devpod.sh/docs/developing-providers/quickstart) +for an introduction into developing your own providers ### Publish your provider @@ -51,18 +60,18 @@ Once you're provider is ready, update to get your provider featured both in the documentation and the UI - ## Deeplinks -DevPod Desktop can handle deep links to perform various actions, like opening or importing workspaces. +DevPod Desktop can handle deep links to perform various actions, like opening or +importing workspaces. The scheme is: protocol: `devpod://` host: `command` searchParams: `foo=bar&fizz=buzz` -resulting in a full url string of `devpod://command?foo=bar&fizz=buzz`. For more information, take a look at the indvidual command sections below. - +resulting in a full url string of `devpod://command?foo=bar&fizz=buzz`. For more +information, take a look at the indvidual command sections below. ### Open Workspace @@ -73,10 +82,10 @@ searchParams: `source` (required), `workspace`, `provider`, `ide` `devpod://open?source=your-url-encoded-source&workspace=my-workspace&provider=docker&ide=vscode` - ### Import Workspace Import a remote DevPod.Pro workspace into your local client host: `import` -searchParams: `workspace_id` (required), `workspace_uid` (required), `devpod_pro_host` (required), `options` +searchParams: `workspace_id` (required), `workspace_uid` (required), +`devpod_pro_host` (required), `options` diff --git a/desktop/README.md b/desktop/README.md index b7dba713c..ea7b5162e 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -5,7 +5,8 @@ ## Development 1. Install [NodeJS](https://nodejs.org/en/) -2. Install [Yarn](https://yarnpkg.com/getting-started/install) and make sure you use yarn v1, by running `yarn set version 1.22.1` +2. Install [Yarn](https://yarnpkg.com/getting-started/install) and make sure you + use yarn v1, by running `yarn set version 1.22.1` 3. Install [Rust](https://www.rust-lang.org/tools/install) 4. Install [Go](https://go.dev/doc/install) 5. Run `./hack/rebuild.sh` from the root directory of the repo @@ -16,27 +17,40 @@ To build the app on Linux, you will need the following dependencies: -```console -sudo apt-get install libappindicator3-1 libgdk-pixbuf2.0-0 libbsd0 libxdmcp6 libwmf-0.2-7 libwmf-0.2-7-gtk libgtk-3-0 libwmf-dev libwebkit2gtk-4.0-37 librust-openssl-sys-dev librust-glib-sys-dev - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev file build-essential +```bash +sudo apt-get install libappindicator3-1 libgdk-pixbuf2.0-0 libbsd0 libxdmcp6 \ + libwmf-0.2-7 libwmf-0.2-7-gtk libgtk-3-0 libwmf-dev libwebkit2gtk-4.0-37 \ + librust-openssl-sys-dev librust-glib-sys-dev +sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev \ + libayatana-appindicator3-dev librsvg2-dev file build-essential ``` ### Additional Information -Make sure all of your dependencies are installed and up to date by running `yarn` and `cd src-tauri && cargo update`. +Make sure all of your dependencies are installed and up to date by running `yarn` +and `cd src-tauri && cargo update`. Frontend code lives in `src` Backend code lives in `src-tauri` -Entrypoint for the application is the `main` function in `src-tauri/main.rs`. It instructs tauri to set up the application, bootstrap the webview and serve our static assets. -As of now, we just bundle all of the javascript into one file, so we don't have any prerendering or code splitting going on. +Entrypoint for the application is the `main` function in `src-tauri/main.rs`. +It instructs tauri to set up the application, bootstrap the webview and serve our +static assets. As of now, we just bundle all of the javascript into one file, so +we don't have any prerendering or code splitting going on. -To spin up the application in development mode, run `yarn tauri dev`. It will report both the frontend webserver output (vite) and the backend logs to your current terminal. -Tauri should automatically restart the app if your backend code changes and vite is responsible for hot module updates in the frontend. +To spin up the application in development mode, run `yarn tauri dev`. It will +report both the frontend webserver output (vite) and the backend logs to your +current terminal. +Tauri should automatically restart the app if your backend code changes and vite +is responsible for hot module updates in the frontend. Enable debug logging to stdout during development with `DEBUG=true yarn tauri dev`. -If you just want to preview the project locally, make sure to disabled the auto update feature by setting `desktop/src-tauri/tauri.conf.json->updater.active=false`. Please be careful not to commit this change later on. -Once you're happy with the current state, give it a spin in release mode by running `yarn tauri build`. You can find the packaged version of the application in the `src-tauri/target/release/{PLATFORM}` folder. +If you just want to preview the project locally, make sure to disabled the auto +update feature by setting `desktop/src-tauri/tauri.conf.json->updater.active=false`. +Please be careful not to commit this change later on. +Once you're happy with the current state, give it a spin in release mode by running +`yarn tauri build`. You can find the packaged version of the application in the +`src-tauri/target/release/{PLATFORM}` folder. ## Check Type Errors @@ -44,12 +58,16 @@ Run `yarn types:check` to check for errors ## Versioning -The apps version is determined by the one in `package.json`. Be careful not to add one in `tauri.conf.json` as it override the current one. +The apps version is determined by the one in `package.json`. Be careful not to add +one in `tauri.conf.json` as it override the current one. You can upgrade the version manually or run `yarn version ...` ## Build desktop app -If your development environment is setup successfully and you're able to run `yarn desktop:dev` without problems, you also should be able to build the app locally by runnning `yarn desktop:build:dev`. -Notice the `:dev` suffix, if you omit this it'll try to build with the updater enabled. This won't work on your machine as it requires sensitive information. +If your development environment is setup successfully and you're able to run +`yarn desktop:dev` without problems, you also should be able to build the app +locally by runnning `yarn desktop:build:dev`. +Notice the `:dev` suffix, if you omit this it'll try to build with the updater +enabled. This won't work on your machine as it requires sensitive information. The output of the command can be found in `desktop/src-tauri/target/release/bundle`. diff --git a/desktop/package.json b/desktop/package.json index f1a9a332a..5047a92f7 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -77,5 +77,6 @@ }, "resolutions": { "lodash": "4.17.21" - } + }, + "packageManager": "yarn@1.22.19" } diff --git a/desktop/src/client/client.ts b/desktop/src/client/client.ts index 140615361..d78cd5260 100644 --- a/desktop/src/client/client.ts +++ b/desktop/src/client/client.ts @@ -205,8 +205,8 @@ class Client { } } - public async selectFromDir(): Promise { - return dialog.open({ directory: true, multiple: false }) + public async selectFromDir(title?: string): Promise { + return dialog.open({ title, directory: true, multiple: false }) } public async selectFromFileYaml(): Promise { @@ -225,6 +225,14 @@ class Client { return fs.copyFile(src, dest) } + public async copyFilePaths(src: string[], dest: string[]) { + return this.copyFile(await path.join(...src), await path.join(...dest)) + } + + public async writeTextFile(targetPath: string[], data: string) { + return fs.writeTextFile(await path.join(...targetPath), data) + } + public async installCLI(force: boolean = false): Promise> { try { await invoke("install_cli", { force }) diff --git a/desktop/src/client/constants.ts b/desktop/src/client/constants.ts index 3f6fc3663..f8d8c0981 100644 --- a/desktop/src/client/constants.ts +++ b/desktop/src/client/constants.ts @@ -32,6 +32,7 @@ export const DEVPOD_COMMAND_GET_WORKSPACE_CONFIG = "get-workspace-config" export const DEVPOD_COMMAND_GET_PROVIDER_NAME = "get-provider-name" export const DEVPOD_COMMAND_GET_PRO_NAME = "get-pro-name" export const DEVPOD_COMMAND_CHECK_PROVIDER_UPDATE = "check-provider-update" +export const DEVPOD_COMMAND_TROUBLESHOOT = "troubleshoot" export const DEVPOD_FLAG_JSON_LOG_OUTPUT = "--log-output=json" export const DEVPOD_FLAG_JSON_OUTPUT = "--output=json" export const DEVPOD_FLAG_OPTION = "--option" diff --git a/desktop/src/client/workspaces/client.ts b/desktop/src/client/workspaces/client.ts index 195df3064..28b5e373c 100644 --- a/desktop/src/client/workspaces/client.ts +++ b/desktop/src/client/workspaces/client.ts @@ -179,6 +179,11 @@ export class WorkspacesClient implements TDebuggable { return this.getStatus(ctx.id) } + public async troubleshoot(ctx: TWorkspaceClientContext) { + const cmd = WorkspaceCommands.TroubleshootWorkspace(ctx.id) + return cmd.run() + } + public async reset( listener: TStreamEventListenerFn | undefined, ctx: TWorkspaceClientContext diff --git a/desktop/src/client/workspaces/workspaceCommands.ts b/desktop/src/client/workspaces/workspaceCommands.ts index 181c3a489..647e72e77 100644 --- a/desktop/src/client/workspaces/workspaceCommands.ts +++ b/desktop/src/client/workspaces/workspaceCommands.ts @@ -17,6 +17,7 @@ import { DEVPOD_COMMAND_STATUS, DEVPOD_COMMAND_STOP, DEVPOD_COMMAND_UP, + DEVPOD_COMMAND_TROUBLESHOOT, DEVPOD_FLAG_DEBUG, DEVPOD_FLAG_DEVCONTAINER_PATH, DEVPOD_FLAG_FORCE, @@ -206,6 +207,10 @@ export class WorkspaceCommands { ]) } + static TroubleshootWorkspace(id: TWorkspaceID) { + return WorkspaceCommands.newCommand([DEVPOD_COMMAND_TROUBLESHOOT, id]) + } + static RemoveWorkspace(id: TWorkspaceID, force?: boolean) { const args = [DEVPOD_COMMAND_DELETE, id, DEVPOD_FLAG_JSON_LOG_OUTPUT] if (force) { diff --git a/desktop/src/lib/useStoreTroubleshoot.ts b/desktop/src/lib/useStoreTroubleshoot.ts new file mode 100644 index 000000000..99a47d7e6 --- /dev/null +++ b/desktop/src/lib/useStoreTroubleshoot.ts @@ -0,0 +1,73 @@ +import { client } from "@/client" +import { TActionObj } from "@/contexts/DevPodContext/action" +import { TWorkspace } from "@/types" +import { useToast } from "@chakra-ui/react" +import { useMutation } from "@tanstack/react-query" +import { ProWorkspaceInstance } from "@/contexts" + +export function useStoreTroubleshoot() { + const toast = useToast() + const { mutate, isLoading: isStoring } = useMutation({ + mutationFn: async ({ + workspace, + workspaceActions, + }: { + workspace: TWorkspace | ProWorkspaceInstance + workspaceActions: TActionObj[] + }) => { + const logFiles = await Promise.all( + workspaceActions.map((action) => client.workspaces.getActionLogFile(action.id)) + ) + + const targetFolder = await client.selectFromDir("Save Troubleshooting Data") + + // user cancelled "save file" dialog + if (targetFolder === null) { + return + } + + const unwrappedLogFiles = logFiles + .filter((f) => f.ok) + .map((f) => f.unwrap() ?? "") + .map((f) => [[f], [targetFolder, f.split("/").pop() ?? ""]]) + // poor mans zip + await Promise.all( + unwrappedLogFiles.map(([src, target]) => client.copyFilePaths(src ?? [], target ?? [])) + ) + + await client.writeTextFile( + [targetFolder, "workspace_actions.json"], + JSON.stringify(workspaceActions, null, 2) + ) + + await client.writeTextFile( + [targetFolder, "workspace.json"], + JSON.stringify(workspace, null, 2) + ) + + const troubleshootOutput = await client.workspaces.troubleshoot({ + id: workspace.id, + actionID: "", + streamID: "", + }) + if (troubleshootOutput.ok) { + await client.writeTextFile( + [targetFolder, "cli_troubleshoot.json"], + troubleshootOutput.unwrap().stdout + ) + } + + client.open(targetFolder) + }, + onError(error) { + toast({ + title: `Failed to save logs: ${error}`, + status: "error", + isClosable: true, + duration: 30_000, // 30 sec + }) + }, + }) + + return { store: mutate, isStoring } +} diff --git a/desktop/src/views/Pro/Workspace/Workspace.tsx b/desktop/src/views/Pro/Workspace/Workspace.tsx index 46e3d7714..37fb8da94 100644 --- a/desktop/src/views/Pro/Workspace/Workspace.tsx +++ b/desktop/src/views/Pro/Workspace/Workspace.tsx @@ -5,6 +5,7 @@ import { useProjectClusters, useTemplates, useWorkspace, + useWorkspaceActions, } from "@/contexts" import { Clock, Folder, Git, Globe, Image, Status } from "@/icons" import { @@ -27,6 +28,7 @@ import { BackToWorkspaces } from "../BackToWorkspaces" import { WorkspaceTabs } from "./Tabs" import { WorkspaceCardHeader } from "./WorkspaceCardHeader" import { WorkspaceStatus } from "./WorkspaceStatus" +import { useStoreTroubleshoot } from "@/lib/useStoreTroubleshoot" export function Workspace() { const params = useParams<{ workspace: string }>() @@ -37,6 +39,7 @@ export function Workspace() { const workspace = useWorkspace(params.workspace) const instance = workspace.data const instanceDisplayName = getDisplayName(instance) + const workspaceActions = useWorkspaceActions(instance?.id) const { modal: stopModal, open: openStopModal } = useStopWorkspaceModal( useCallback( @@ -115,6 +118,8 @@ export function Workspace() { ) } + const { store: storeTroubleshoot } = useStoreTroubleshoot() + const canStop = instance.status?.lastWorkspaceStatus != "Busy" && instance.status?.lastWorkspaceStatus != "Stopped" @@ -130,6 +135,15 @@ export function Workspace() { const lastActivity = getLastActivity(instance) + const handleTroubleshootClicked = useCallback(() => { + if (workspace.data && workspaceActions) { + storeTroubleshoot({ + workspace: workspace.data, + workspaceActions: workspaceActions, + }) + } + }, [storeTroubleshoot, workspace.data, workspaceActions]) + return ( <> @@ -143,6 +157,7 @@ export function Workspace() { onRebuildClicked={openRebuildModal} onResetClicked={openResetModal} onStopClicked={!canStop ? openStopModal : workspace.stop} + onTroubleshootClicked={handleTroubleshootClicked} /> diff --git a/desktop/src/views/Pro/Workspace/WorkspaceCardHeader.tsx b/desktop/src/views/Pro/Workspace/WorkspaceCardHeader.tsx index ac9695d1d..6bb8db056 100644 --- a/desktop/src/views/Pro/Workspace/WorkspaceCardHeader.tsx +++ b/desktop/src/views/Pro/Workspace/WorkspaceCardHeader.tsx @@ -3,6 +3,7 @@ import { ProWorkspaceInstance, useSettings } from "@/contexts" import { ArrowCycle, ArrowPath, + Cog, Ellipsis, GitBranch, GitCommit, @@ -80,6 +81,7 @@ type TControlsProps = Readonly<{ onResetClicked: VoidFunction onRebuildClicked: VoidFunction onDeleteClicked: VoidFunction + onTroubleshootClicked: VoidFunction }> export function Controls({ onOpenClicked, @@ -87,6 +89,7 @@ export function Controls({ onResetClicked, onRebuildClicked, onDeleteClicked, + onTroubleshootClicked, }: TControlsProps) { const { ides, defaultIDE } = useIDEs() const settings = useSettings() @@ -151,6 +154,12 @@ export function Controls({ } onClick={onResetClicked} isDisabled={false}> Reset... + } + onClick={onTroubleshootClicked}> + Troubleshoot + (instanceName) const instance = workspace.data const instanceDisplayName = getDisplayName(instance) + const workspaceActions = useWorkspaceActions(instance?.id) + const navigate = useNavigate() const { modal: stopModal, open: openStopModal } = useStopWorkspaceModal( @@ -85,6 +88,8 @@ export function WorkspaceInstanceCard({ instanceName, host }: TWorkspaceInstance ) ) + const { store: storeTroubleshoot } = useStoreTroubleshoot() + const { parameters, template } = useMemo<{ parameters: readonly TParameterWithValue[] template: ManagementV1DevPodWorkspaceTemplate | undefined @@ -115,6 +120,15 @@ export function WorkspaceInstanceCard({ instanceName, host }: TWorkspaceInstance navigate(Routes.toProWorkspace(host, instance.id)) } + const handleTroubleshootClicked = useCallback(() => { + if (instance && workspaceActions) { + storeTroubleshoot({ + workspace: instance, + workspaceActions: workspaceActions, + }) + } + }, [storeTroubleshoot, instance, workspaceActions]) + const templateRef = instance.spec?.templateRef const isRunning = instance.status?.lastWorkspaceStatus === "Running" // TODO: Types @@ -137,6 +151,7 @@ export function WorkspaceInstanceCard({ instanceName, host }: TWorkspaceInstance onRebuildClicked={openRebuildModal} onResetClicked={openResetModal} onStopClicked={isRunning ? openStopModal : workspace.stop} + onTroubleshootClicked={handleTroubleshootClicked} /> diff --git a/desktop/src/views/Workspaces/WorkspaceCard.tsx b/desktop/src/views/Workspaces/WorkspaceCard.tsx index 580c04eb8..a18dce95d 100644 --- a/desktop/src/views/Workspaces/WorkspaceCard.tsx +++ b/desktop/src/views/Workspaces/WorkspaceCard.tsx @@ -42,6 +42,7 @@ import { Template } from "@/icons" import { HiServerStack } from "react-icons/hi2" import { TOptionWithID, mergeOptionDefinitions } from "../Providers/helpers" import { processDisplayOptions } from "../Providers/AddProvider/useProviderOptions" +import { useStoreTroubleshoot } from "@/lib/useStoreTroubleshoot" type TWorkspaceCardProps = Readonly<{ workspaceID: TWorkspaceID @@ -111,6 +112,7 @@ export function WorkspaceCard({ workspaceID, isSelected, onSelectionChange }: TW onOpen: handleChangeOptionsClicked, onClose: onChangeOptionsClose, } = useDisclosure() + const { store: storeTroubleshoot } = useStoreTroubleshoot() const [provider] = useProvider(workspace.data?.provider?.name) const [ideName, setIdeName] = useState(() => { @@ -134,6 +136,15 @@ export function WorkspaceCard({ workspaceID, isSelected, onSelectionChange }: TW navigateToAction(actionID) }, [navigateToAction, workspace]) + const handleTroubleshootClicked = useCallback(() => { + if (workspaceActions && workspace.data) { + storeTroubleshoot({ + workspace: workspace.data, + workspaceActions: workspaceActions, + }) + } + }, [storeTroubleshoot, workspace.data, workspaceActions]) + const hasError = useMemo(() => { if (!workspaceActions?.length || workspaceActions[0]?.status !== "error") { return false @@ -246,6 +257,7 @@ export function WorkspaceCard({ workspaceID, isSelected, onSelectionChange }: TW onDeleteClicked={openDeleteModal} onStopClicked={openStopModal} onLogsClicked={handleLogsClicked} + onTroubleshootClicked={handleTroubleshootClicked} onChangeOptionsClicked={handleChangeOptionsClicked} /> }> diff --git a/desktop/src/views/Workspaces/WorkspaceControls.tsx b/desktop/src/views/Workspaces/WorkspaceControls.tsx index 397fffc3a..3775933a7 100644 --- a/desktop/src/views/Workspaces/WorkspaceControls.tsx +++ b/desktop/src/views/Workspaces/WorkspaceControls.tsx @@ -27,6 +27,7 @@ import { ArrowCycle, ArrowPath, CommandLine, + Cog, Ellipsis, Pause, Play, @@ -51,6 +52,7 @@ type TWorkspaceControlsProps = Readonly<{ onDeleteClicked: VoidFunction onStopClicked: VoidFunction onLogsClicked: VoidFunction + onTroubleshootClicked: VoidFunction onChangeOptionsClicked?: VoidFunction }> export function WorkspaceControls({ @@ -68,6 +70,7 @@ export function WorkspaceControls({ onDeleteClicked, onStopClicked, onLogsClicked, + onTroubleshootClicked, onChangeOptionsClicked, }: TWorkspaceControlsProps) { const [[proInstances]] = useProInstances() @@ -208,6 +211,12 @@ export function WorkspaceControls({ onClick={onLogsClicked}> Logs + } + onClick={onTroubleshootClicked}> + Troubleshoot +