Skip to content

Commit e7cad82

Browse files
committed
handle dev containers
1 parent 195151a commit e7cad82

File tree

4 files changed

+117
-38
lines changed

4 files changed

+117
-38
lines changed

src/commands.ts

+58-30
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ export class Commands {
491491
} else {
492492
workspaceOwner = args[0] as string
493493
workspaceName = args[1] as string
494-
workspaceAgent = args[2] as string
494+
workspaceAgent = args[2] as string | undefined
495495
folderPath = args[3] as string | undefined
496496
openRecent = args[4] as boolean | undefined
497497
}
@@ -522,11 +522,11 @@ export class Commands {
522522

523523
const workspaceOwner = args[0] as string
524524
const workspaceName = args[1] as string
525-
const workspaceAgent = undefined // args[2] is reserved, but we do not support multiple agents yet.
525+
const workspaceAgent = args[2] as string | undefined
526526
const devContainerName = args[3] as string
527527
const devContainerFolder = args[4] as string
528528

529-
await openDevContainer(baseUrl, workspaceOwner, workspaceName, workspaceAgent, devContainerName, devContainerFolder)
529+
await this.openDevContainerInner(baseUrl, workspaceOwner, workspaceName, workspaceAgent, devContainerName, devContainerFolder)
530530
}
531531

532532
/**
@@ -652,33 +652,61 @@ export class Commands {
652652
reuseWindow: !newWindow,
653653
})
654654
}
655-
}
656655

657-
async function openDevContainer(
658-
baseUrl: string,
659-
workspaceOwner: string,
660-
workspaceName: string,
661-
workspaceAgent: string | undefined,
662-
devContainerName: string,
663-
devContainerFolder: string,
664-
) {
665-
const remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
666-
667-
const devContainer = Buffer.from(JSON.stringify({ containerName: devContainerName }), "utf-8").toString("hex")
668-
const devContainerAuthority = `attached-container+${devContainer}@${remoteAuthority}`
669-
670-
let newWindow = true
671-
if (!vscode.workspace.workspaceFolders?.length) {
672-
newWindow = false
673-
}
656+
private async openDevContainerInner(
657+
baseUrl: string,
658+
workspaceOwner: string,
659+
workspaceName: string,
660+
workspaceAgent: string | undefined,
661+
devContainerName: string,
662+
devContainerFolder: string,
663+
) {
664+
let remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
665+
666+
if (workspaceAgent) {
667+
let sshConfig
668+
try {
669+
// Fetch (or get defaults) for the SSH config.
670+
sshConfig = await fetchSSHConfig(this.restClient, this.vscodeProposed)
671+
} catch (error) {
672+
const message = getErrorMessage(error, "no response from the server")
673+
this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
674+
this.vscodeProposed.window.showErrorMessage("Failed to open workspace", {
675+
detail: message,
676+
modal: true,
677+
useCustom: true,
678+
})
679+
return
680+
}
681+
682+
const coderConnectAddr = await maybeCoderConnectAddr(
683+
workspaceAgent,
684+
workspaceName,
685+
workspaceOwner,
686+
sshConfig.hostname_suffix,
687+
)
688+
if (coderConnectAddr) {
689+
remoteAuthority = `ssh-remote+${coderConnectAddr}`
690+
}
691+
}
692+
693+
const devContainer = Buffer.from(JSON.stringify({ containerName: devContainerName }), "utf-8").toString("hex")
694+
const devContainerAuthority = `attached-container+${devContainer}@${remoteAuthority}`
674695

675-
await vscode.commands.executeCommand(
676-
"vscode.openFolder",
677-
vscode.Uri.from({
678-
scheme: "vscode-remote",
679-
authority: devContainerAuthority,
680-
path: devContainerFolder,
681-
}),
682-
newWindow,
683-
)
696+
let newWindow = true
697+
if (!vscode.workspace.workspaceFolders?.length) {
698+
newWindow = false
699+
}
700+
701+
await vscode.commands.executeCommand(
702+
"vscode.openFolder",
703+
vscode.Uri.from({
704+
scheme: "vscode-remote",
705+
authority: devContainerAuthority,
706+
path: devContainerFolder,
707+
}),
708+
newWindow,
709+
)
710+
}
684711
}
712+

src/remote.ts

+26-4
Original file line numberDiff line numberDiff line change
@@ -528,10 +528,32 @@ export class Remote {
528528
sshConfigResponse.hostname_suffix,
529529
)
530530
if (coderConnectAddr) {
531-
await vscode.commands.executeCommand("vscode.newWindow", {
532-
remoteAuthority: `ssh-remote+${coderConnectAddr}`,
533-
reuseWindow: true,
534-
})
531+
// Find the path of the current workspace, which will have the same authority
532+
const folderPath = this.vscodeProposed.workspace.workspaceFolders
533+
?.find(folder => folder.uri.authority === remoteAuthority)
534+
?.uri.path;
535+
let newRemoteAuthority = `ssh-remote+${coderConnectAddr}`
536+
if (parts.containerNameHex) {
537+
newRemoteAuthority = `attached-container+${parts.containerNameHex}@${newRemoteAuthority}`
538+
}
539+
540+
if (folderPath) {
541+
await vscode.commands.executeCommand(
542+
"vscode.openFolder",
543+
vscode.Uri.from({
544+
scheme: "vscode-remote",
545+
authority: newRemoteAuthority,
546+
path: folderPath,
547+
}),
548+
//`ForceNewWindow`
549+
false,
550+
)
551+
} else {
552+
await vscode.commands.executeCommand("vscode.newWindow", {
553+
remoteAuthority: newRemoteAuthority,
554+
reuseWindow: true,
555+
})
556+
}
535557
}
536558

537559
// Returning the URL and token allows the plugin to authenticate its own

src/util.test.ts

+14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ it("ignore unrelated authorities", async () => {
99
"vscode://ssh-remote+coder-vscode-test--foo--bar",
1010
"vscode://ssh-remote+coder-vscode-foo--bar",
1111
"vscode://ssh-remote+coder--foo--bar",
12+
"vscode://attached-container+namehash@ssh-remote+dev.foo.admin.coder"
1213
]
1314
for (const test of tests) {
1415
expect(parseRemoteAuthority(test)).toBe(null)
@@ -29,34 +30,47 @@ it("should error on invalid authorities", async () => {
2930

3031
it("should parse authority", async () => {
3132
expect(parseRemoteAuthority("vscode://ssh-remote+coder-vscode--foo--bar")).toStrictEqual({
33+
containerNameHex: undefined,
3234
agent: "",
3335
host: "coder-vscode--foo--bar",
3436
label: "",
3537
username: "foo",
3638
workspace: "bar",
3739
})
3840
expect(parseRemoteAuthority("vscode://ssh-remote+coder-vscode--foo--bar--baz")).toStrictEqual({
41+
containerNameHex: undefined,
3942
agent: "baz",
4043
host: "coder-vscode--foo--bar--baz",
4144
label: "",
4245
username: "foo",
4346
workspace: "bar",
4447
})
4548
expect(parseRemoteAuthority("vscode://ssh-remote+coder-vscode.dev.coder.com--foo--bar")).toStrictEqual({
49+
containerNameHex: undefined,
4650
agent: "",
4751
host: "coder-vscode.dev.coder.com--foo--bar",
4852
label: "dev.coder.com",
4953
username: "foo",
5054
workspace: "bar",
5155
})
5256
expect(parseRemoteAuthority("vscode://ssh-remote+coder-vscode.dev.coder.com--foo--bar--baz")).toStrictEqual({
57+
containerNameHex: undefined,
5358
agent: "baz",
5459
host: "coder-vscode.dev.coder.com--foo--bar--baz",
5560
label: "dev.coder.com",
5661
username: "foo",
5762
workspace: "bar",
5863
})
5964
expect(parseRemoteAuthority("vscode://ssh-remote+coder-vscode.dev.coder.com--foo--bar.baz")).toStrictEqual({
65+
containerNameHex: undefined,
66+
agent: "baz",
67+
host: "coder-vscode.dev.coder.com--foo--bar.baz",
68+
label: "dev.coder.com",
69+
username: "foo",
70+
workspace: "bar",
71+
})
72+
expect(parseRemoteAuthority("vscode://attached-container+namehash@ssh-remote+coder-vscode.dev.coder.com--foo--bar.baz")).toStrictEqual({
73+
containerNameHex: "namehash",
6074
agent: "baz",
6175
host: "coder-vscode.dev.coder.com--foo--bar.baz",
6276
label: "dev.coder.com",

src/util.ts

+19-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { lookup } from "dns"
22
import ipRangeCheck from "ip-range-check"
3+
import { ssh } from "node-forge"
34
import * as os from "os"
45
import url from "url"
56
import { promisify } from "util"
67

78
export interface AuthorityParts {
9+
containerNameHex: string | undefined
810
agent: string | undefined
911
host: string
1012
label: string
@@ -24,14 +26,26 @@ export const AuthorityPrefix = "coder-vscode"
2426
* Throw an error if the host is invalid.
2527
*/
2628
export function parseRemoteAuthority(authority: string): AuthorityParts | null {
27-
// The authority looks like: vscode://ssh-remote+<ssh host name>
28-
const authorityParts = authority.split("+")
29+
// The Dev Container authority looks like: vscode://attached-container+containerNameHex@ssh-remote+<ssh host name>
30+
// The SSH authority looks like: vscode://ssh-remote+<ssh host name>
31+
const authorityParts = authority.split("@")
32+
let containerNameHex = undefined
33+
let sshAuthority
34+
if (authorityParts.length == 1) {
35+
sshAuthority = authorityParts[0]
36+
} else if (authorityParts.length == 2 && authorityParts[0].includes("attached-container+")) {
37+
sshAuthority = authorityParts[1]
38+
containerNameHex = authorityParts[0].split("+")[1]
39+
} else {
40+
return null
41+
}
42+
const sshAuthorityParts = sshAuthority.split("+")
2943

3044
// We create SSH host names in a format matching:
3145
// coder-vscode(--|.)<username>--<workspace>(--|.)<agent?>
3246
// The agent can be omitted; the user will be prompted for it instead.
3347
// Anything else is unrelated to Coder and can be ignored.
34-
const parts = authorityParts[1].split("--")
48+
const parts = sshAuthorityParts[1].split("--")
3549
if (parts.length <= 1 || (parts[0] !== AuthorityPrefix && !parts[0].startsWith(`${AuthorityPrefix}.`))) {
3650
return null
3751
}
@@ -56,8 +70,9 @@ export function parseRemoteAuthority(authority: string): AuthorityParts | null {
5670
}
5771

5872
return {
73+
containerNameHex: containerNameHex,
5974
agent: agent,
60-
host: authorityParts[1],
75+
host: sshAuthorityParts[1],
6176
label: parts[0].replace(/^coder-vscode\.?/, ""),
6277
username: parts[1],
6378
workspace: workspace,

0 commit comments

Comments
 (0)