From 0a747856d06ae164fa07228a196f9478ecb1d5fc Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 2 Apr 2024 11:17:06 -0500 Subject: [PATCH 1/4] feat: auto update workspace if required by template --- src/remote.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/remote.ts b/src/remote.ts index 4f504e79..7835eec6 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -137,11 +137,22 @@ export class Remote { let buildComplete: undefined | (() => void) if (this.storage.workspace.latest_build.status === "stopped") { + // If the workspace requires the latest active template version, we should attempt + // to update that here. + // TODO: If param set changes, what do we do?? + const versionID = this.storage.workspace.template_require_active_version + ? // Use the latest template version + this.storage.workspace.template_active_version_id + : // Default to not updating the workspace if not required. + this.storage.workspace.latest_build.template_version_id + this.vscodeProposed.window.withProgress( { location: vscode.ProgressLocation.Notification, cancellable: false, - title: "Starting workspace...", + title: this.storage.workspace.template_require_active_version + ? "Updating workspace..." + : "Starting workspace...", }, () => new Promise((r) => { @@ -150,10 +161,7 @@ export class Remote { ) this.storage.workspace = { ...this.storage.workspace, - latest_build: await startWorkspace( - this.storage.workspace.id, - this.storage.workspace.latest_build.template_version_id, - ), + latest_build: await startWorkspace(this.storage.workspace.id, versionID), } } From 0eef32845326f39df5536f9d5a4030591ada1b3b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 2 Apr 2024 12:27:26 -0500 Subject: [PATCH 2/4] raise modal error when creating a workspace --- src/error.ts | 12 ++++++++++++ src/remote.ts | 27 ++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/error.ts b/src/error.ts index 0e9babd9..31723fd3 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,4 +1,5 @@ import { isAxiosError } from "axios" +import { isApiError, isApiErrorResponse } from "coder/site/src/api/errors" import * as forge from "node-forge" import * as tls from "tls" import * as vscode from "vscode" @@ -157,3 +158,14 @@ export class CertificateError extends Error { } } } + +// getErrorDetail is copied from coder/site, but changes the default return. +export const getErrorDetail = (error: unknown): string | undefined | null => { + if (isApiError(error)) { + return error.response.data.detail + } + if (isApiErrorResponse(error)) { + return error.detail + } + return null +} diff --git a/src/remote.ts b/src/remote.ts index 7835eec6..0680c2ad 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -9,6 +9,7 @@ import { getDeploymentSSHConfig, getTemplateVersion, } from "coder/site/src/api/api" +import { getErrorMessage } from "coder/site/src/api/errors" import { ProvisionerJobLog, Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated" import EventSource from "eventsource" import find from "find-process" @@ -26,6 +27,7 @@ import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport" import { Storage } from "./storage" import { supportsCoderAgentLogDirFlag } from "./version" import { WorkspaceAction } from "./workspaceAction" +import { getErrorDetail } from "./error" export class Remote { // Prefix is a magic string that is prepended to SSH hosts to indicate that @@ -159,9 +161,32 @@ export class Remote { buildComplete = r }), ) + + let latestBuild + try { + latestBuild = await startWorkspace(this.storage.workspace.id, versionID) + } catch (error) { + if (!isAxiosError(error)) { + throw error + } + + const msg = getErrorMessage(error, "unknown") + const detail = getErrorDetail(error) + + await this.vscodeProposed.window.showInformationMessage("Workspace failed to start!", { + modal: true, + detail: `Error, remote session will be closed.\nMessage: ${msg}\nDetail: ${detail}`, + }) + + // Always close remote + await this.closeRemote() + + return + } + this.storage.workspace = { ...this.storage.workspace, - latest_build: await startWorkspace(this.storage.workspace.id, versionID), + latest_build: latestBuild, } } From 69fee732c07ede9a9fe1fa15d6b024e2432549be Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 2 Apr 2024 12:42:10 -0500 Subject: [PATCH 3/4] catch all api errors in top level. Rather than a 1 off for start workspace. --- src/extension.ts | 44 +++++++++++++++++++++++++++++++++++--------- src/remote.ts | 27 +++------------------------ 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index dfcfb324..07fc4126 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,5 @@ "use strict" -import axios from "axios" +import axios, { isAxiosError } from "axios" import { getAuthenticatedUser } from "coder/site/src/api/api" import fs from "fs" import * as https from "https" @@ -7,10 +7,11 @@ import * as module from "module" import * as os from "os" import * as vscode from "vscode" import { Commands } from "./commands" -import { CertificateError } from "./error" +import { CertificateError, getErrorDetail } from "./error" import { Remote } from "./remote" import { Storage } from "./storage" import { WorkspaceQuery, WorkspaceProvider } from "./workspacesProvider" +import { getErrorMessage } from "coder/site/src/api/errors" export async function activate(ctx: vscode.ExtensionContext): Promise { // The Remote SSH extension's proposed APIs are used to override the SSH host @@ -199,13 +200,38 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { try { await remote.setup(vscodeProposed.env.remoteAuthority) } catch (ex) { - if (ex instanceof CertificateError) { - return await ex.showModal("Failed to open workspace") + switch (true) { + case ex instanceof CertificateError: + await ex.showModal("Failed to open workspace") + break + case isAxiosError(ex): + { + const msg = getErrorMessage(ex, "") + const detail = getErrorDetail(ex) + const urlString = axios.getUri(ex.response?.config) + let path = urlString + try { + path = new URL(urlString).pathname + } catch (e) { + // ignore, default to full url + } + + await vscodeProposed.window.showErrorMessage("Failed to open workspace", { + detail: `API ${ex.response?.config.method?.toUpperCase()} to '${path}' failed with code ${ex.response?.status}.\nMessage: ${msg}\nDetail: ${detail}`, + modal: true, + useCustom: true, + }) + } + break + default: + await vscodeProposed.window.showErrorMessage("Failed to open workspace", { + detail: (ex as string).toString(), + modal: true, + useCustom: true, + }) } - await vscodeProposed.window.showErrorMessage("Failed to open workspace", { - detail: (ex as string).toString(), - modal: true, - useCustom: true, - }) + + // Always close remote session when we fail to open a workspace. + await remote.closeRemote() } } diff --git a/src/remote.ts b/src/remote.ts index 0680c2ad..58f1953f 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -21,13 +21,13 @@ import prettyBytes from "pretty-bytes" import * as semver from "semver" import * as vscode from "vscode" import * as ws from "ws" +import { getErrorDetail } from "./error" import { getHeaderCommand } from "./headers" import { SSHConfig, SSHValues, defaultSSHConfigResponse, mergeSSHConfigValues } from "./sshConfig" import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport" import { Storage } from "./storage" import { supportsCoderAgentLogDirFlag } from "./version" import { WorkspaceAction } from "./workspaceAction" -import { getErrorDetail } from "./error" export class Remote { // Prefix is a magic string that is prepended to SSH hosts to indicate that @@ -162,28 +162,7 @@ export class Remote { }), ) - let latestBuild - try { - latestBuild = await startWorkspace(this.storage.workspace.id, versionID) - } catch (error) { - if (!isAxiosError(error)) { - throw error - } - - const msg = getErrorMessage(error, "unknown") - const detail = getErrorDetail(error) - - await this.vscodeProposed.window.showInformationMessage("Workspace failed to start!", { - modal: true, - detail: `Error, remote session will be closed.\nMessage: ${msg}\nDetail: ${detail}`, - }) - - // Always close remote - await this.closeRemote() - - return - } - + const latestBuild = await startWorkspace(this.storage.workspace.id, versionID) this.storage.workspace = { ...this.storage.workspace, latest_build: latestBuild, @@ -829,7 +808,7 @@ export class Remote { } // closeRemote ends the current remote session. - private async closeRemote() { + public async closeRemote() { await vscode.commands.executeCommand("workbench.action.remote.close") } From 94e2680941ff7adc97320fbdfadf9b7063ccd92a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 2 Apr 2024 12:45:03 -0500 Subject: [PATCH 4/4] lint --- src/extension.ts | 2 +- src/remote.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 07fc4126..c499d576 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ "use strict" import axios, { isAxiosError } from "axios" import { getAuthenticatedUser } from "coder/site/src/api/api" +import { getErrorMessage } from "coder/site/src/api/errors" import fs from "fs" import * as https from "https" import * as module from "module" @@ -11,7 +12,6 @@ import { CertificateError, getErrorDetail } from "./error" import { Remote } from "./remote" import { Storage } from "./storage" import { WorkspaceQuery, WorkspaceProvider } from "./workspacesProvider" -import { getErrorMessage } from "coder/site/src/api/errors" export async function activate(ctx: vscode.ExtensionContext): Promise { // The Remote SSH extension's proposed APIs are used to override the SSH host diff --git a/src/remote.ts b/src/remote.ts index 58f1953f..9b19bee2 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -9,7 +9,6 @@ import { getDeploymentSSHConfig, getTemplateVersion, } from "coder/site/src/api/api" -import { getErrorMessage } from "coder/site/src/api/errors" import { ProvisionerJobLog, Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated" import EventSource from "eventsource" import find from "find-process" @@ -21,7 +20,6 @@ import prettyBytes from "pretty-bytes" import * as semver from "semver" import * as vscode from "vscode" import * as ws from "ws" -import { getErrorDetail } from "./error" import { getHeaderCommand } from "./headers" import { SSHConfig, SSHValues, defaultSSHConfigResponse, mergeSSHConfigValues } from "./sshConfig" import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport"