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

Lines changed: 51 additions & 5 deletions
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

Lines changed: 56 additions & 0 deletions
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 sign in 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

Lines changed: 13 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy