Skip to content

Commit

Permalink
Start workspaces by shelling out to CLI
Browse files Browse the repository at this point in the history
Replace the REST-API-based start flow with one that shells out to the
coder CLI.

Signed-off-by: Aaron Lehmann <[email protected]>
  • Loading branch information
aaronlehmann committed Nov 18, 2024
1 parent da1aaed commit 59ac05c
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 37 deletions.
58 changes: 43 additions & 15 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { spawn, ChildProcessWithoutNullStreams } from "child_process"
import { Api } from "coder/site/src/api/api"
import { ProvisionerJobLog, Workspace } from "coder/site/src/api/typesGenerated"
import fs from "fs/promises"
Expand Down Expand Up @@ -122,29 +123,56 @@ export async function makeCoderSdk(baseUrl: string, token: string | undefined, s
/**
* Start or update a workspace and return the updated workspace.
*/
export async function startWorkspaceIfStoppedOrFailed(restClient: Api, workspace: Workspace): Promise<Workspace> {
// 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 = workspace.template_require_active_version
? // Use the latest template version
workspace.template_active_version_id
: // Default to not updating the workspace if not required.
workspace.latest_build.template_version_id

export async function startWorkspaceIfStoppedOrFailed(
restClient: Api,
binPath: string,
workspace: Workspace,
writeEmitter: vscode.EventEmitter<string>,
): Promise<Workspace> {
// Before we start a workspace, we make an initial request to check it's not already started
const updatedWorkspace = await restClient.getWorkspace(workspace.id)

if (!["stopped", "failed"].includes(updatedWorkspace.latest_build.status)) {
return updatedWorkspace
}

const latestBuild = await restClient.startWorkspace(updatedWorkspace.id, versionID)
return new Promise((resolve, reject) => {
const startProcess: ChildProcessWithoutNullStreams = spawn(binPath, [
"start",
"--yes",
workspace.owner_name + "/" + workspace.name,
])

startProcess.stdout.on("data", (data: Buffer) => {
data
.toString()
.split(/\r*\n/)
.forEach((line: string) => {
if (line !== "") {
writeEmitter.fire(line.toString() + "\r\n")
}
})
})

startProcess.stderr.on("data", (data: Buffer) => {
data
.toString()
.split(/\r*\n/)
.forEach((line: string) => {
if (line !== "") {
writeEmitter.fire(line.toString() + "\r\n")
}
})
})

return {
...updatedWorkspace,
latest_build: latestBuild,
}
startProcess.on("close", (code: number) => {
if (code === 0) {
resolve(restClient.getWorkspace(workspace.id))
} else {
reject(new Error(`"coder start" process exited with code ${code}`))
}
})
})
}

/**
Expand Down
55 changes: 33 additions & 22 deletions src/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ export class Remote {
/**
* Try to get the workspace running. Return undefined if the user canceled.
*/
private async maybeWaitForRunning(restClient: Api, workspace: Workspace): Promise<Workspace | undefined> {
private async maybeWaitForRunning(
restClient: Api,
workspace: Workspace,
binPath: string,
): Promise<Workspace | undefined> {
// Maybe already running?
if (workspace.latest_build.status === "running") {
return workspace
Expand All @@ -63,6 +67,28 @@ export class Remote {
let terminal: undefined | vscode.Terminal
let attempts = 0

function initWriteEmitterAndTerminal(): vscode.EventEmitter<string> {
if (!writeEmitter) {
writeEmitter = new vscode.EventEmitter<string>()
}
if (!terminal) {
terminal = vscode.window.createTerminal({
name: "Build Log",
location: vscode.TerminalLocation.Panel,
// Spin makes this gear icon spin!
iconPath: new vscode.ThemeIcon("gear~spin"),
pty: {
onDidWrite: writeEmitter.event,
close: () => undefined,
open: () => undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as Partial<vscode.Pseudoterminal> as any,
})
terminal.show(true)
}
return writeEmitter
}

try {
// Show a notification while we wait.
return await this.vscodeProposed.window.withProgress(
Expand All @@ -78,33 +104,17 @@ export class Remote {
case "pending":
case "starting":
case "stopping":
if (!writeEmitter) {
writeEmitter = new vscode.EventEmitter<string>()
}
if (!terminal) {
terminal = vscode.window.createTerminal({
name: "Build Log",
location: vscode.TerminalLocation.Panel,
// Spin makes this gear icon spin!
iconPath: new vscode.ThemeIcon("gear~spin"),
pty: {
onDidWrite: writeEmitter.event,
close: () => undefined,
open: () => undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as Partial<vscode.Pseudoterminal> as any,
})
terminal.show(true)
}
writeEmitter = initWriteEmitterAndTerminal()
this.storage.writeToCoderOutputChannel(`Waiting for ${workspaceName}...`)
workspace = await waitForBuild(restClient, writeEmitter, workspace)
break
case "stopped":
if (!(await this.confirmStart(workspaceName))) {
return undefined
}
writeEmitter = initWriteEmitterAndTerminal()
this.storage.writeToCoderOutputChannel(`Starting ${workspaceName}...`)
workspace = await startWorkspaceIfStoppedOrFailed(restClient, workspace)
workspace = await startWorkspaceIfStoppedOrFailed(restClient, binPath, workspace, writeEmitter)
break
case "failed":
// On a first attempt, we will try starting a failed workspace
Expand All @@ -113,8 +123,9 @@ export class Remote {
if (!(await this.confirmStart(workspaceName))) {
return undefined
}
writeEmitter = initWriteEmitterAndTerminal()
this.storage.writeToCoderOutputChannel(`Starting ${workspaceName}...`)
workspace = await startWorkspaceIfStoppedOrFailed(restClient, workspace)
workspace = await startWorkspaceIfStoppedOrFailed(restClient, binPath, workspace, writeEmitter)
break
}
// Otherwise fall through and error.
Expand Down Expand Up @@ -292,7 +303,7 @@ export class Remote {
disposables.push(this.registerLabelFormatter(remoteAuthority, workspace.owner_name, workspace.name))

// If the workspace is not in a running state, try to get it running.
const updatedWorkspace = await this.maybeWaitForRunning(workspaceRestClient, workspace)
const updatedWorkspace = await this.maybeWaitForRunning(workspaceRestClient, workspace, binaryPath)
if (!updatedWorkspace) {
// User declined to start the workspace.
await this.closeRemote()
Expand Down

0 comments on commit 59ac05c

Please sign in to comment.