Skip to content

Commit 95b5b4e

Browse files
committed
feat: coder connect integration
1 parent 1fb0f02 commit 95b5b4e

File tree

4 files changed

+117
-16
lines changed

4 files changed

+117
-16
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@
316316
"node-forge": "^1.3.1",
317317
"pretty-bytes": "^6.1.1",
318318
"proxy-agent": "^6.4.0",
319+
"range_check": "^3.2.0",
319320
"semver": "^7.6.2",
320321
"ua-parser-js": "^1.0.38",
321322
"ws": "^8.18.1",

src/commands.ts

+96-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import { isAxiosError } from "axios"
12
import { Api } from "coder/site/src/api/api"
23
import { getErrorMessage } from "coder/site/src/api/errors"
34
import { User, Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
5+
import { lookup } from "dns"
6+
import { inRange } from "range_check"
7+
import { promisify } from "util"
48
import * as vscode from "vscode"
59
import { makeCoderSdk, needToken } from "./api"
610
import { extractAgents } from "./api-helper"
@@ -392,14 +396,33 @@ export class Commands {
392396
if (!baseUrl) {
393397
throw new Error("You are not logged in")
394398
}
395-
await openWorkspace(
396-
baseUrl,
397-
treeItem.workspaceOwner,
398-
treeItem.workspaceName,
399-
treeItem.workspaceAgent,
400-
treeItem.workspaceFolderPath,
401-
true,
402-
)
399+
400+
let agent = treeItem.workspaceAgent
401+
if (!agent) {
402+
// `openFromSidebar` is only callable on agents or single-agent workspaces,
403+
// where this will always be set.
404+
return
405+
}
406+
407+
try {
408+
await openWorkspace(
409+
this.restClient,
410+
baseUrl,
411+
treeItem.workspaceOwner,
412+
treeItem.workspaceName,
413+
agent,
414+
treeItem.workspaceFolderPath,
415+
true,
416+
)
417+
} catch (err) {
418+
const message = getErrorMessage(err, "no response from the server")
419+
this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
420+
this.vscodeProposed.window.showErrorMessage("Failed to open workspace", {
421+
detail: message,
422+
modal: true,
423+
useCustom: true,
424+
})
425+
}
403426
} else {
404427
// If there is no tree item, then the user manually ran this command.
405428
// Default to the regular open instead.
@@ -491,12 +514,30 @@ export class Commands {
491514
} else {
492515
workspaceOwner = args[0] as string
493516
workspaceName = args[1] as string
494-
// workspaceAgent is reserved for args[2], but multiple agents aren't supported yet.
517+
workspaceAgent = args[2] as string
495518
folderPath = args[3] as string | undefined
496519
openRecent = args[4] as boolean | undefined
497520
}
498521

499-
await openWorkspace(baseUrl, workspaceOwner, workspaceName, workspaceAgent, folderPath, openRecent)
522+
try {
523+
await openWorkspace(
524+
this.restClient,
525+
baseUrl,
526+
workspaceOwner,
527+
workspaceName,
528+
workspaceAgent,
529+
folderPath,
530+
openRecent,
531+
)
532+
} catch (err) {
533+
const message = getErrorMessage(err, "no response from the server")
534+
this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
535+
this.vscodeProposed.window.showErrorMessage("Failed to open workspace", {
536+
detail: message,
537+
modal: true,
538+
useCustom: true,
539+
})
540+
}
500541
}
501542

502543
/**
@@ -547,16 +588,42 @@ export class Commands {
547588
* both to the Remote SSH plugin in the form of a remote authority URI.
548589
*/
549590
async function openWorkspace(
591+
restClient: Api,
550592
baseUrl: string,
551593
workspaceOwner: string,
552594
workspaceName: string,
553-
workspaceAgent: string | undefined,
595+
workspaceAgent: string,
554596
folderPath: string | undefined,
555597
openRecent: boolean | undefined,
556598
) {
557-
// A workspace can have multiple agents, but that's handled
558-
// when opening a workspace unless explicitly specified.
559-
const remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
599+
let remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
600+
601+
let hostnameSuffix = "coder"
602+
try {
603+
const sshConfig = await restClient.getDeploymentSSHConfig()
604+
// If the field is undefined, it's an older server, and always 'coder'
605+
hostnameSuffix = sshConfig.hostname_suffix ?? hostnameSuffix
606+
} catch (error) {
607+
if (!isAxiosError(error)) {
608+
throw error
609+
}
610+
switch (error.response?.status) {
611+
case 404: {
612+
// Likely a very old deployment, just use the default.
613+
break
614+
}
615+
case 401: {
616+
throw error
617+
}
618+
default:
619+
throw error
620+
}
621+
}
622+
623+
const coderConnectAddr = await maybeCoderConnectAddr(workspaceAgent, workspaceName, workspaceOwner, hostnameSuffix)
624+
if (coderConnectAddr) {
625+
remoteAuthority = `ssh-remote+${coderConnectAddr}`
626+
}
560627

561628
let newWindow = true
562629
// Open in the existing window if no workspaces are open.
@@ -616,6 +683,21 @@ async function openWorkspace(
616683
})
617684
}
618685

686+
async function maybeCoderConnectAddr(
687+
agent: string,
688+
workspace: string,
689+
owner: string,
690+
hostnameSuffix: string,
691+
): Promise<string | undefined> {
692+
const coderConnectHostname = `${agent}.${workspace}.${owner}.${hostnameSuffix}`
693+
try {
694+
const res = await promisify(lookup)(coderConnectHostname)
695+
return res.family == 6 && inRange(res.address, "fd60:627a:a42b::/48") ? coderConnectHostname : undefined
696+
} catch {
697+
return undefined
698+
}
699+
}
700+
619701
async function openDevContainer(
620702
baseUrl: string,
621703
workspaceOwner: string,

src/workspacesProvider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ export class WorkspaceTreeItem extends OpenableTreeItem {
353353
showOwner ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.Expanded,
354354
workspace.owner_name,
355355
workspace.name,
356-
undefined,
356+
agents.length > 0 ? agents[0].name : undefined,
357357
agents[0]?.expanded_directory,
358358
agents.length > 1 ? "coderWorkspaceMultipleAgents" : "coderWorkspaceSingleAgent",
359359
)

yarn.lock

+19-1
Original file line numberDiff line numberDiff line change
@@ -1749,7 +1749,7 @@ [email protected]:
17491749

17501750
"coder@https://github.com/coder/coder#main":
17511751
version "0.0.0"
1752-
resolved "https://github.com/coder/coder#%3Cspan%20class="x x-first x-last">3ac844ad3d341d2910542b83d4f33df7bd0be85e"
1752+
resolved "https://github.com/coder/coder#%3Cspan%20class="x x-first x-last">f8971bb3cc01d81b3085b2b3c9253d8d340d125c"
17531753

17541754
collapse-white-space@^1.0.2:
17551755
version "1.0.6"
@@ -3441,6 +3441,16 @@ ip-address@^9.0.5:
34413441
jsbn "1.1.0"
34423442
sprintf-js "^1.1.3"
34433443

3444+
ip6@^0.2.10:
3445+
version "0.2.11"
3446+
resolved "https://registry.yarnpkg.com/ip6/-/ip6-0.2.11.tgz#b7cf71864ef16c7418c29f7b1f2f5db892a189ec"
3447+
integrity sha512-OmTP7FyIp+ZoNvZ7Xr97bWrCgypa3BeuYuRFNTOPT8Y11cxMW1pW1VC70kHZP1onSHHMotADcjdg5QyECiIMUw==
3448+
3449+
ipaddr.js@^2.2.0:
3450+
version "2.2.0"
3451+
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8"
3452+
integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==
3453+
34443454
irregular-plurals@^2.0.0:
34453455
version "2.0.0"
34463456
resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-2.0.0.tgz#39d40f05b00f656d0b7fa471230dd3b714af2872"
@@ -4834,6 +4844,14 @@ randombytes@^2.1.0:
48344844
dependencies:
48354845
safe-buffer "^5.1.0"
48364846

4847+
range_check@^3.2.0:
4848+
version "3.2.0"
4849+
resolved "https://registry.yarnpkg.com/range_check/-/range_check-3.2.0.tgz#6ef17940bb382a7fb905ecda8204f2f28ce7f61d"
4850+
integrity sha512-JxiMqvzQJJLt5vaKSUm7f++UkDM1TuMbkQsqRZJYaSvvCTTVtoUMkE/rm+ZNgLXNFAQPhO74WgMPHJaxz/JOEA==
4851+
dependencies:
4852+
ip6 "^0.2.10"
4853+
ipaddr.js "^2.2.0"
4854+
48374855
rc@^1.2.7:
48384856
version "1.2.8"
48394857
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"

0 commit comments

Comments
 (0)