@@ -42,6 +42,7 @@ func (r *RootCmd) openVSCode() *serpent.Command {
42
42
generateToken bool
43
43
testOpenError bool
44
44
appearanceConfig codersdk.AppearanceConfig
45
+ containerName string
45
46
)
46
47
47
48
client := new (codersdk.Client )
@@ -112,27 +113,48 @@ func (r *RootCmd) openVSCode() *serpent.Command {
112
113
if len (inv .Args ) > 1 {
113
114
directory = inv .Args [1 ]
114
115
}
115
- directory , err = resolveAgentAbsPath (workspaceAgent .ExpandedDirectory , directory , workspaceAgent .OperatingSystem , insideThisWorkspace )
116
- if err != nil {
117
- return xerrors .Errorf ("resolve agent path: %w" , err )
118
- }
119
116
120
- u := & url. URL {
121
- Scheme : "vscode" ,
122
- Host : "coder.coder-remote" ,
123
- Path : "/open" ,
124
- }
117
+ if containerName != "" {
118
+ containers , err := client . WorkspaceAgentListContainers ( ctx , workspaceAgent . ID , map [ string ] string { "devcontainer.local_folder" : "" })
119
+ if err != nil {
120
+ return xerrors . Errorf ( "list workspace agent containers: %w" , err )
121
+ }
125
122
126
- qp := url. Values {}
123
+ var foundContainer bool
127
124
128
- qp .Add ("url" , client .URL .String ())
129
- qp .Add ("owner" , workspace .OwnerName )
130
- qp .Add ("workspace" , workspace .Name )
131
- qp .Add ("agent" , workspaceAgent .Name )
132
- if directory != "" {
133
- qp .Add ("folder" , directory )
125
+ for _ , container := range containers .Containers {
126
+ if container .FriendlyName != containerName {
127
+ continue
128
+ }
129
+
130
+ foundContainer = true
131
+
132
+ if directory == "" {
133
+ localFolder , ok := container .Labels ["devcontainer.local_folder" ]
134
+ if ! ok {
135
+ return xerrors .New ("container missing `devcontainer.local_folder` label" )
136
+ }
137
+
138
+ directory , ok = container .Volumes [localFolder ]
139
+ if ! ok {
140
+ return xerrors .New ("container missing volume for `devcontainer.local_folder`" )
141
+ }
142
+ }
143
+
144
+ break
145
+ }
146
+
147
+ if ! foundContainer {
148
+ return xerrors .New ("no container found" )
149
+ }
150
+ }
151
+
152
+ directory , err = resolveAgentAbsPath (workspaceAgent .ExpandedDirectory , directory , workspaceAgent .OperatingSystem , insideThisWorkspace )
153
+ if err != nil {
154
+ return xerrors .Errorf ("resolve agent path: %w" , err )
134
155
}
135
156
157
+ var token string
136
158
// We always set the token if we believe we can open without
137
159
// printing the URI, otherwise the token must be explicitly
138
160
// requested as it will be printed in plain text.
@@ -145,10 +167,31 @@ func (r *RootCmd) openVSCode() *serpent.Command {
145
167
if err != nil {
146
168
return xerrors .Errorf ("create API key: %w" , err )
147
169
}
148
- qp . Add ( " token" , apiKey .Key )
170
+ token = apiKey .Key
149
171
}
150
172
151
- u .RawQuery = qp .Encode ()
173
+ var (
174
+ u * url.URL
175
+ qp url.Values
176
+ )
177
+ if containerName != "" {
178
+ u , qp = buildVSCodeWorkspaceDevContainerLink (
179
+ token ,
180
+ client .URL .String (),
181
+ workspace ,
182
+ workspaceAgent ,
183
+ containerName ,
184
+ directory ,
185
+ )
186
+ } else {
187
+ u , qp = buildVSCodeWorkspaceLink (
188
+ token ,
189
+ client .URL .String (),
190
+ workspace ,
191
+ workspaceAgent ,
192
+ directory ,
193
+ )
194
+ }
152
195
153
196
openingPath := workspaceName
154
197
if directory != "" {
@@ -204,6 +247,13 @@ func (r *RootCmd) openVSCode() *serpent.Command {
204
247
),
205
248
Value : serpent .BoolOf (& generateToken ),
206
249
},
250
+ {
251
+ Flag : "container" ,
252
+ FlagShorthand : "c" ,
253
+ Description : "Container name to connect to in the workspace." ,
254
+ Value : serpent .StringOf (& containerName ),
255
+ Hidden : true , // Hidden until this features is at least in beta.
256
+ },
207
257
{
208
258
Flag : "test.open-error" ,
209
259
Description : "Don't run the open command." ,
@@ -344,6 +394,65 @@ func (r *RootCmd) openApp() *serpent.Command {
344
394
return cmd
345
395
}
346
396
397
+ func buildVSCodeWorkspaceLink (
398
+ token string ,
399
+ clientURL string ,
400
+ workspace codersdk.Workspace ,
401
+ workspaceAgent codersdk.WorkspaceAgent ,
402
+ directory string ,
403
+ ) (* url.URL , url.Values ) {
404
+ qp := url.Values {}
405
+ qp .Add ("url" , clientURL )
406
+ qp .Add ("owner" , workspace .OwnerName )
407
+ qp .Add ("workspace" , workspace .Name )
408
+ qp .Add ("agent" , workspaceAgent .Name )
409
+
410
+ if directory != "" {
411
+ qp .Add ("folder" , directory )
412
+ }
413
+
414
+ if token != "" {
415
+ qp .Add ("token" , token )
416
+ }
417
+
418
+ return & url.URL {
419
+ Scheme : "vscode" ,
420
+ Host : "coder.coder-remote" ,
421
+ Path : "/open" ,
422
+ RawQuery : qp .Encode (),
423
+ }, qp
424
+ }
425
+
426
+ func buildVSCodeWorkspaceDevContainerLink (
427
+ token string ,
428
+ clientURL string ,
429
+ workspace codersdk.Workspace ,
430
+ workspaceAgent codersdk.WorkspaceAgent ,
431
+ containerName string ,
432
+ containerFolder string ,
433
+ ) (* url.URL , url.Values ) {
434
+ containerFolder = filepath .ToSlash (containerFolder )
435
+
436
+ qp := url.Values {}
437
+ qp .Add ("url" , clientURL )
438
+ qp .Add ("owner" , workspace .OwnerName )
439
+ qp .Add ("workspace" , workspace .Name )
440
+ qp .Add ("agent" , workspaceAgent .Name )
441
+ qp .Add ("devContainerName" , containerName )
442
+ qp .Add ("devContainerFolder" , containerFolder )
443
+
444
+ if token != "" {
445
+ qp .Add ("token" , token )
446
+ }
447
+
448
+ return & url.URL {
449
+ Scheme : "vscode" ,
450
+ Host : "coder.coder-remote" ,
451
+ Path : "/openDevContainer" ,
452
+ RawQuery : qp .Encode (),
453
+ }, qp
454
+ }
455
+
347
456
// waitForAgentCond uses the watch workspace API to update the agent information
348
457
// until the condition is met.
349
458
func waitForAgentCond (ctx context.Context , client * codersdk.Client , workspace codersdk.Workspace , workspaceAgent codersdk.WorkspaceAgent , cond func (codersdk.WorkspaceAgent ) bool ) (codersdk.Workspace , codersdk.WorkspaceAgent , error ) {
0 commit comments