@@ -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,46 @@ 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
+ foundContainer = true
128
+
129
+ if directory == "" {
130
+ localFolder , ok := container .Labels ["devcontainer.local_folder" ]
131
+ if ! ok {
132
+ return xerrors .New ("container missing `devcontainer.local_folder` label" )
133
+ }
134
+
135
+ directory , ok = container .Volumes [localFolder ]
136
+ if ! ok {
137
+ return xerrors .New ("container missing volume for `devcontainer.local_folder`" )
138
+ }
139
+ }
140
+
141
+ break
142
+ }
143
+ }
144
+
145
+ if ! foundContainer {
146
+ return xerrors .New ("no container found" )
147
+ }
148
+ }
149
+
150
+ directory , err = resolveAgentAbsPath (workspaceAgent .ExpandedDirectory , directory , workspaceAgent .OperatingSystem , insideThisWorkspace )
151
+ if err != nil {
152
+ return xerrors .Errorf ("resolve agent path: %w" , err )
134
153
}
135
154
155
+ var token string
136
156
// We always set the token if we believe we can open without
137
157
// printing the URI, otherwise the token must be explicitly
138
158
// requested as it will be printed in plain text.
@@ -145,10 +165,31 @@ func (r *RootCmd) openVSCode() *serpent.Command {
145
165
if err != nil {
146
166
return xerrors .Errorf ("create API key: %w" , err )
147
167
}
148
- qp . Add ( " token" , apiKey .Key )
168
+ token = apiKey .Key
149
169
}
150
170
151
- u .RawQuery = qp .Encode ()
171
+ var (
172
+ u * url.URL
173
+ qp url.Values
174
+ )
175
+ if containerName != "" {
176
+ u , qp = buildVSCodeWorkspaceDevContainerLink (
177
+ token ,
178
+ client .URL .String (),
179
+ workspace ,
180
+ workspaceAgent ,
181
+ containerName ,
182
+ directory ,
183
+ )
184
+ } else {
185
+ u , qp = buildVSCodeWorkspaceLink (
186
+ token ,
187
+ client .URL .String (),
188
+ workspace ,
189
+ workspaceAgent ,
190
+ directory ,
191
+ )
192
+ }
152
193
153
194
openingPath := workspaceName
154
195
if directory != "" {
@@ -204,6 +245,12 @@ func (r *RootCmd) openVSCode() *serpent.Command {
204
245
),
205
246
Value : serpent .BoolOf (& generateToken ),
206
247
},
248
+ {
249
+ Flag : "container" ,
250
+ FlagShorthand : "c" ,
251
+ Description : "Container name to connect to in the workspace." ,
252
+ Value : serpent .StringOf (& containerName ),
253
+ },
207
254
{
208
255
Flag : "test.open-error" ,
209
256
Description : "Don't run the open command." ,
@@ -344,6 +391,63 @@ func (r *RootCmd) openApp() *serpent.Command {
344
391
return cmd
345
392
}
346
393
394
+ func buildVSCodeWorkspaceLink (
395
+ token string ,
396
+ clientURL string ,
397
+ workspace codersdk.Workspace ,
398
+ workspaceAgent codersdk.WorkspaceAgent ,
399
+ directory string ,
400
+ ) (* url.URL , url.Values ) {
401
+ qp := url.Values {}
402
+ qp .Add ("url" , clientURL )
403
+ qp .Add ("owner" , workspace .OwnerName )
404
+ qp .Add ("workspace" , workspace .Name )
405
+ qp .Add ("agent" , workspaceAgent .Name )
406
+
407
+ if directory != "" {
408
+ qp .Add ("folder" , directory )
409
+ }
410
+
411
+ if token != "" {
412
+ qp .Add ("token" , token )
413
+ }
414
+
415
+ return & url.URL {
416
+ Scheme : "vscode" ,
417
+ Host : "coder.coder-remote" ,
418
+ Path : "/open" ,
419
+ RawQuery : qp .Encode (),
420
+ }, qp
421
+ }
422
+
423
+ func buildVSCodeWorkspaceDevContainerLink (
424
+ token string ,
425
+ clientURL string ,
426
+ workspace codersdk.Workspace ,
427
+ workspaceAgent codersdk.WorkspaceAgent ,
428
+ containerName string ,
429
+ containerFolder string ,
430
+ ) (* url.URL , url.Values ) {
431
+ qp := url.Values {}
432
+ qp .Add ("url" , clientURL )
433
+ qp .Add ("owner" , workspace .OwnerName )
434
+ qp .Add ("workspace" , workspace .Name )
435
+ qp .Add ("agent" , workspaceAgent .Name )
436
+ qp .Add ("devContainerName" , containerName )
437
+ qp .Add ("devContainerFolder" , containerFolder )
438
+
439
+ if token != "" {
440
+ qp .Add ("token" , token )
441
+ }
442
+
443
+ return & url.URL {
444
+ Scheme : "vscode" ,
445
+ Host : "coder.coder-remote" ,
446
+ Path : "/openDevContainer" ,
447
+ RawQuery : qp .Encode (),
448
+ }, qp
449
+ }
450
+
347
451
// waitForAgentCond uses the watch workspace API to update the agent information
348
452
// until the condition is met.
349
453
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