@@ -17,15 +17,11 @@ import { getHeaderCommand } from "./headers"
17
17
import { SSHConfig , SSHValues , mergeSSHConfigValues } from "./sshConfig"
18
18
import { computeSSHProperties , sshSupportsSetEnv } from "./sshSupport"
19
19
import { Storage } from "./storage"
20
- import { toSafeHost } from "./util"
20
+ import { parseRemoteAuthority } from "./util"
21
21
import { supportsCoderAgentLogDirFlag } from "./version"
22
22
import { WorkspaceAction } from "./workspaceAction"
23
23
24
24
export class Remote {
25
- // Prefix is a magic string that is prepended to SSH hosts to indicate that
26
- // they should be handled by this extension.
27
- public static readonly Prefix = "coder-vscode"
28
-
29
25
public constructor (
30
26
private readonly vscodeProposed : typeof vscode ,
31
27
private readonly storage : Storage ,
@@ -34,34 +30,19 @@ export class Remote {
34
30
) { }
35
31
36
32
public async setup ( remoteAuthority : string ) : Promise < vscode . Disposable | undefined > {
37
- const authorityParts = remoteAuthority . split ( "+" )
38
- // If the URI passed doesn't have the proper prefix ignore it. We don't need
39
- // to do anything special, because this isn't trying to open a Coder
40
- // workspace.
41
- if ( ! authorityParts [ 1 ] . startsWith ( Remote . Prefix ) ) {
33
+ const parts = parseRemoteAuthority ( remoteAuthority )
34
+ if ( ! parts ) {
35
+ // Not a Coder host.
42
36
return
43
37
}
44
- const sshAuthority = authorityParts [ 1 ] . substring ( Remote . Prefix . length )
45
-
46
- // Authorities are in one of two formats:
47
- // coder-vscode--<username>--<workspace>--<agent> (old style)
48
- // coder-vscode.<label>--<username>--<workspace>--<agent>
49
- // The agent can be omitted; the user will be prompted for it instead.
50
- const parts = sshAuthority . split ( "--" )
51
- if ( parts . length !== 2 && parts . length !== 3 ) {
52
- throw new Error ( `Invalid Coder SSH authority. Must be: <username>--<workspace>--<agent?>` )
53
- }
54
- const workspaceName = `${ parts [ 0 ] } /${ parts [ 1 ] } `
55
38
56
- // It is possible to connect to any previously connected workspace, which
57
- // might not belong to the deployment the plugin is currently logged into.
58
- // For that reason, create a separate REST client instead of using the
59
- // global one generally used by the plugin. For now this is not actually
60
- // useful because we are using the the current URL and token anyway, but in
61
- // a future PR we will store these per deployment and grab the right one
62
- // based on the host name of the workspace to which we are connecting.
63
- const baseUrlRaw = this . storage . getUrl ( )
64
- if ( ! baseUrlRaw ) {
39
+ const workspaceName = `${ parts . username } /${ parts . workspace } `
40
+
41
+ // Get the URL and token belonging to this host.
42
+ const { url : baseUrlRaw , token } = await this . storage . readCliConfig ( parts . label )
43
+
44
+ // It could be that the cli config was deleted. If so, ask for the url.
45
+ if ( ! baseUrlRaw || ! token ) {
65
46
const result = await this . vscodeProposed . window . showInformationMessage (
66
47
"You are not logged in..." ,
67
48
{
@@ -76,15 +57,16 @@ export class Remote {
76
57
await this . closeRemote ( )
77
58
} else {
78
59
// Log in then try again.
79
- await vscode . commands . executeCommand ( "coder.login" )
60
+ await vscode . commands . executeCommand ( "coder.login" , baseUrlRaw )
80
61
await this . setup ( remoteAuthority )
81
62
}
82
63
return
83
64
}
84
65
85
- const baseUrl = new URL ( baseUrlRaw )
86
- const safeHost = toSafeHost ( baseUrlRaw ) // Deployment label.
87
- const token = await this . storage . getSessionToken ( )
66
+ // It is possible to connect to any previously connected workspace, which
67
+ // might not belong to the deployment the plugin is currently logged into.
68
+ // For that reason, create a separate REST client instead of using the
69
+ // global one generally used by the plugin.
88
70
const restClient = await makeCoderSdk ( baseUrlRaw , token , this . storage )
89
71
// Store for use in commands.
90
72
this . commands . workspaceRestClient = restClient
@@ -116,7 +98,7 @@ export class Remote {
116
98
// Next is to find the workspace from the URI scheme provided.
117
99
let workspace : Workspace
118
100
try {
119
- workspace = await restClient . getWorkspaceByOwnerAndName ( parts [ 0 ] , parts [ 1 ] )
101
+ workspace = await restClient . getWorkspaceByOwnerAndName ( parts . username , parts . workspace )
120
102
this . commands . workspace = workspace
121
103
} catch ( error ) {
122
104
if ( ! isAxiosError ( error ) ) {
@@ -235,24 +217,30 @@ export class Remote {
235
217
path += `&after=${ logs [ logs . length - 1 ] . id } `
236
218
}
237
219
await new Promise < void > ( ( resolve , reject ) => {
238
- const proto = baseUrl . protocol === "https:" ? "wss:" : "ws:"
239
- const socket = new ws . WebSocket ( new URL ( `${ proto } //${ baseUrl . host } ${ path } ` ) , {
240
- headers : {
241
- "Coder-Session-Token" : token ,
242
- } ,
243
- } )
244
- socket . binaryType = "nodebuffer"
245
- socket . on ( "message" , ( data ) => {
246
- const buf = data as Buffer
247
- const log = JSON . parse ( buf . toString ( ) ) as ProvisionerJobLog
248
- writeEmitter . fire ( log . output + "\r\n" )
249
- } )
250
- socket . on ( "error" , ( err ) => {
251
- reject ( err )
252
- } )
253
- socket . on ( "close" , ( ) => {
254
- resolve ( )
255
- } )
220
+ try {
221
+ const baseUrl = new URL ( baseUrlRaw )
222
+ const proto = baseUrl . protocol === "https:" ? "wss:" : "ws:"
223
+ const socket = new ws . WebSocket ( new URL ( `${ proto } //${ baseUrl . host } ${ path } ` ) , {
224
+ headers : {
225
+ "Coder-Session-Token" : token ,
226
+ } ,
227
+ } )
228
+ socket . binaryType = "nodebuffer"
229
+ socket . on ( "message" , ( data ) => {
230
+ const buf = data as Buffer
231
+ const log = JSON . parse ( buf . toString ( ) ) as ProvisionerJobLog
232
+ writeEmitter . fire ( log . output + "\r\n" )
233
+ } )
234
+ socket . on ( "error" , ( err ) => {
235
+ reject ( err )
236
+ } )
237
+ socket . on ( "close" , ( ) => {
238
+ resolve ( )
239
+ } )
240
+ } catch ( error ) {
241
+ // If this errors, it is probably a malformed URL.
242
+ reject ( error )
243
+ }
256
244
} )
257
245
writeEmitter . fire ( "Build complete" )
258
246
workspace = await restClient . getWorkspace ( workspace . id )
@@ -268,7 +256,7 @@ export class Remote {
268
256
`This workspace is stopped!` ,
269
257
{
270
258
modal : true ,
271
- detail : `Click below to start and open ${ parts [ 0 ] } / ${ parts [ 1 ] } .` ,
259
+ detail : `Click below to start and open ${ workspaceName } .` ,
272
260
useCustom : true ,
273
261
} ,
274
262
"Start Workspace" ,
@@ -286,28 +274,26 @@ export class Remote {
286
274
return acc . concat ( resource . agents || [ ] )
287
275
} , [ ] as WorkspaceAgent [ ] )
288
276
289
- let agent : WorkspaceAgent | undefined
290
-
291
- if ( parts . length === 2 ) {
277
+ // With no agent specified, pick the first one. Otherwise choose the
278
+ // matching agent.
279
+ let agent : WorkspaceAgent
280
+ if ( ! parts . agent ) {
292
281
if ( agents . length === 1 ) {
293
282
agent = agents [ 0 ]
283
+ } else {
284
+ // TODO: Show the agent selector here instead.
285
+ throw new Error ( "Invalid Coder SSH authority. An agent must be specified when there are multiple." )
294
286
}
295
-
296
- // If there are multiple agents, we should select one here! TODO: Support
297
- // multiple agents!
298
- }
299
-
300
- if ( ! agent ) {
301
- const matchingAgents = agents . filter ( ( agent ) => agent . name === parts [ 2 ] )
287
+ } else {
288
+ const matchingAgents = agents . filter ( ( agent ) => agent . name === parts . agent )
302
289
if ( matchingAgents . length !== 1 ) {
303
- // TODO: Show the agent selector here instead!
304
- throw new Error ( ` Invalid Coder SSH authority. Agent not found!` )
290
+ // TODO: Show the agent selector here instead.
291
+ throw new Error ( " Invalid Coder SSH authority. Agent not found." )
305
292
}
306
293
agent = matchingAgents [ 0 ]
307
294
}
308
295
309
296
// Do some janky setting manipulation.
310
- const hostname = authorityParts [ 1 ]
311
297
const remotePlatforms = this . vscodeProposed . workspace
312
298
. getConfiguration ( )
313
299
. get < Record < string , string > > ( "remote.SSH.remotePlatform" , { } )
@@ -330,8 +316,8 @@ export class Remote {
330
316
// Add the remote platform for this host to bypass a step where VS Code asks
331
317
// the user for the platform.
332
318
let mungedPlatforms = false
333
- if ( ! remotePlatforms [ hostname ] || remotePlatforms [ hostname ] !== agent . operating_system ) {
334
- remotePlatforms [ hostname ] = agent . operating_system
319
+ if ( ! remotePlatforms [ parts . host ] || remotePlatforms [ parts . host ] !== agent . operating_system ) {
320
+ remotePlatforms [ parts . host ] = agent . operating_system
335
321
settingsContent = jsonc . applyEdits (
336
322
settingsContent ,
337
323
jsonc . modify ( settingsContent , [ "remote.SSH.remotePlatform" ] , remotePlatforms , { } ) ,
@@ -512,7 +498,7 @@ export class Remote {
512
498
// If we didn't write to the SSH config file, connecting would fail with
513
499
// "Host not found".
514
500
try {
515
- await this . updateSSHConfig ( restClient , safeHost , authorityParts [ 1 ] , hasCoderLogs )
501
+ await this . updateSSHConfig ( restClient , parts . label , parts . host , hasCoderLogs )
516
502
} catch ( error ) {
517
503
this . storage . writeToCoderOutputChannel ( `Failed to configure SSH: ${ error } ` )
518
504
throw error
0 commit comments