Skip to content

Commit 1922834

Browse files
feat: add ability to attach to devcontainers (#463)
1 parent a001bea commit 1922834

File tree

3 files changed

+120
-5
lines changed

3 files changed

+120
-5
lines changed

src/commands.ts

+51-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { makeCoderSdk, needToken } from "./api"
66
import { extractAgents } from "./api-helper"
77
import { CertificateError } from "./error"
88
import { Storage } from "./storage"
9-
import { AuthorityPrefix, toSafeHost } from "./util"
9+
import { toRemoteAuthority, toSafeHost } from "./util"
1010
import { OpenableTreeItem } from "./workspacesProvider"
1111

1212
export class Commands {
@@ -499,6 +499,26 @@ export class Commands {
499499
await openWorkspace(baseUrl, workspaceOwner, workspaceName, workspaceAgent, folderPath, openRecent)
500500
}
501501

502+
/**
503+
* Open a devcontainer from a workspace belonging to the currently logged-in deployment.
504+
*
505+
* Throw if not logged into a deployment.
506+
*/
507+
public async openDevContainer(...args: string[]): Promise<void> {
508+
const baseUrl = this.restClient.getAxiosInstance().defaults.baseURL
509+
if (!baseUrl) {
510+
throw new Error("You are not logged in")
511+
}
512+
513+
const workspaceOwner = args[0] as string
514+
const workspaceName = args[1] as string
515+
const workspaceAgent = undefined // args[2] is reserved, but we do not support multiple agents yet.
516+
const devContainerName = args[3] as string
517+
const devContainerFolder = args[4] as string
518+
519+
await openDevContainer(baseUrl, workspaceOwner, workspaceName, workspaceAgent, devContainerName, devContainerFolder)
520+
}
521+
502522
/**
503523
* Update the current workspace. If there is no active workspace connection,
504524
* this is a no-op.
@@ -536,10 +556,7 @@ async function openWorkspace(
536556
) {
537557
// A workspace can have multiple agents, but that's handled
538558
// when opening a workspace unless explicitly specified.
539-
let remoteAuthority = `ssh-remote+${AuthorityPrefix}.${toSafeHost(baseUrl)}--${workspaceOwner}--${workspaceName}`
540-
if (workspaceAgent) {
541-
remoteAuthority += `.${workspaceAgent}`
542-
}
559+
const remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
543560

544561
let newWindow = true
545562
// Open in the existing window if no workspaces are open.
@@ -598,3 +615,32 @@ async function openWorkspace(
598615
reuseWindow: !newWindow,
599616
})
600617
}
618+
619+
async function openDevContainer(
620+
baseUrl: string,
621+
workspaceOwner: string,
622+
workspaceName: string,
623+
workspaceAgent: string | undefined,
624+
devContainerName: string,
625+
devContainerFolder: string,
626+
) {
627+
const remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
628+
629+
const devContainer = Buffer.from(JSON.stringify({ containerName: devContainerName }), "utf-8").toString("hex")
630+
const devContainerAuthority = `attached-container+${devContainer}@${remoteAuthority}`
631+
632+
let newWindow = true
633+
if (!vscode.workspace.workspaceFolders?.length) {
634+
newWindow = false
635+
}
636+
637+
await vscode.commands.executeCommand(
638+
"vscode.openFolder",
639+
vscode.Uri.from({
640+
scheme: "vscode-remote",
641+
authority: devContainerAuthority,
642+
path: devContainerFolder,
643+
}),
644+
newWindow,
645+
)
646+
}

src/extension.ts

+56
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,61 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
111111
await storage.configureCli(toSafeHost(url), url, token)
112112

113113
vscode.commands.executeCommand("coder.open", owner, workspace, agent, folder, openRecent)
114+
} else if (uri.path === "/openDevContainer") {
115+
const workspaceOwner = params.get("owner")
116+
const workspaceName = params.get("workspace")
117+
const workspaceAgent = params.get("agent")
118+
const devContainerName = params.get("devContainerName")
119+
const devContainerFolder = params.get("devContainerFolder")
120+
121+
if (!workspaceOwner) {
122+
throw new Error("workspace owner must be specified as a query parameter")
123+
}
124+
125+
if (!workspaceName) {
126+
throw new Error("workspace name must be specified as a query parameter")
127+
}
128+
129+
if (!devContainerName) {
130+
throw new Error("dev container name must be specified as a query parameter")
131+
}
132+
133+
if (!devContainerFolder) {
134+
throw new Error("dev container folder must be specified as a query parameter")
135+
}
136+
137+
// We are not guaranteed that the URL we currently have is for the URL
138+
// this workspace belongs to, or that we even have a URL at all (the
139+
// queries will default to localhost) so ask for it if missing.
140+
// Pre-populate in case we do have the right URL so the user can just
141+
// hit enter and move on.
142+
const url = await commands.maybeAskUrl(params.get("url"), storage.getUrl())
143+
if (url) {
144+
restClient.setHost(url)
145+
await storage.setUrl(url)
146+
} else {
147+
throw new Error("url must be provided or specified as a query parameter")
148+
}
149+
150+
// If the token is missing we will get a 401 later and the user will be
151+
// prompted to # again, so we do not need to ensure it is set now.
152+
// For non-token auth, we write a blank token since the `vscodessh`
153+
// command currently always requires a token file. However, if there is
154+
// a query parameter for non-token auth go ahead and use it anyway; all
155+
// that really matters is the file is created.
156+
const token = needToken() ? params.get("token") : (params.get("token") ?? "")
157+
158+
// Store on disk to be used by the cli.
159+
await storage.configureCli(toSafeHost(url), url, token)
160+
161+
vscode.commands.executeCommand(
162+
"coder.openDevContainer",
163+
workspaceOwner,
164+
workspaceName,
165+
workspaceAgent,
166+
devContainerName,
167+
devContainerFolder,
168+
)
114169
} else {
115170
throw new Error(`Unknown path ${uri.path}`)
116171
}
@@ -123,6 +178,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
123178
vscode.commands.registerCommand("coder.login", commands.login.bind(commands))
124179
vscode.commands.registerCommand("coder.logout", commands.logout.bind(commands))
125180
vscode.commands.registerCommand("coder.open", commands.open.bind(commands))
181+
vscode.commands.registerCommand("coder.openDevContainer", commands.openDevContainer.bind(commands))
126182
vscode.commands.registerCommand("coder.openFromSidebar", commands.openFromSidebar.bind(commands))
127183
vscode.commands.registerCommand("coder.workspace.update", commands.updateWorkspace.bind(commands))
128184
vscode.commands.registerCommand("coder.createWorkspace", commands.createWorkspace.bind(commands))

src/util.ts

+13
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ export function parseRemoteAuthority(authority: string): AuthorityParts | null {
6161
}
6262
}
6363

64+
export function toRemoteAuthority(
65+
baseUrl: string,
66+
workspaceOwner: string,
67+
workspaceName: string,
68+
workspaceAgent: string | undefined,
69+
): string {
70+
let remoteAuthority = `ssh-remote+${AuthorityPrefix}.${toSafeHost(baseUrl)}--${workspaceOwner}--${workspaceName}`
71+
if (workspaceAgent) {
72+
remoteAuthority += `.${workspaceAgent}`
73+
}
74+
return remoteAuthority
75+
}
76+
6477
/**
6578
* Given a URL, return the host in a format that is safe to write.
6679
*/

0 commit comments

Comments
 (0)