@@ -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 ) {