@@ -11,7 +11,9 @@ import (
11
11
"runtime"
12
12
"slices"
13
13
"strings"
14
+ "time"
14
15
16
+ "github.com/google/uuid"
15
17
"github.com/skratchdot/open-golang/open"
16
18
"golang.org/x/xerrors"
17
19
@@ -42,7 +44,6 @@ func (r *RootCmd) openVSCode() *serpent.Command {
42
44
generateToken bool
43
45
testOpenError bool
44
46
appearanceConfig codersdk.AppearanceConfig
45
- containerName string
46
47
)
47
48
48
49
client := new (codersdk.Client )
@@ -71,14 +72,78 @@ func (r *RootCmd) openVSCode() *serpent.Command {
71
72
// need to wait for the agent to start.
72
73
workspaceQuery := inv .Args [0 ]
73
74
autostart := true
74
- workspace , workspaceAgent , err := getWorkspaceAndAgent (ctx , inv , client , autostart , workspaceQuery )
75
+ workspace , workspaceAgent , otherWorkspaceAgents , err := getWorkspaceAndAgent (ctx , inv , client , autostart , workspaceQuery )
75
76
if err != nil {
76
77
return xerrors .Errorf ("get workspace and agent: %w" , err )
77
78
}
78
79
79
80
workspaceName := workspace .Name + "." + workspaceAgent .Name
80
81
insideThisWorkspace := insideAWorkspace && inWorkspaceName == workspaceName
81
82
83
+ // To properly work with devcontainers, VS Code has to connect to
84
+ // parent workspace agent. It will then proceed to enter the
85
+ // container given the correct parameters. There is inherently no
86
+ // dependency on the devcontainer agent in this scenario, but
87
+ // relying on it simplifies the logic and ensures the devcontainer
88
+ // is ready. To eliminate the dependency we would need to know that
89
+ // a sub-agent that hasn't been created yet may be a devcontainer,
90
+ // and thus will be created at a later time as well as expose the
91
+ // container folder on the API response.
92
+ var parentWorkspaceAgent codersdk.WorkspaceAgent
93
+ var devcontainer codersdk.WorkspaceAgentDevcontainer
94
+ if workspaceAgent .ParentID .Valid {
95
+ // This is likely a devcontainer agent, so we need to find the
96
+ // parent workspace agent as well as the devcontainer.
97
+ for _ , otherAgent := range otherWorkspaceAgents {
98
+ if otherAgent .ID == workspaceAgent .ParentID .UUID {
99
+ parentWorkspaceAgent = otherAgent
100
+ break
101
+ }
102
+ }
103
+ if parentWorkspaceAgent .ID == uuid .Nil {
104
+ return xerrors .Errorf ("parent workspace agent %s not found" , workspaceAgent .ParentID .UUID )
105
+ }
106
+
107
+ printedWaiting := false
108
+ for {
109
+ resp , err := client .WorkspaceAgentListContainers (ctx , parentWorkspaceAgent .ID , nil )
110
+ if err != nil {
111
+ return xerrors .Errorf ("list parent workspace agent containers: %w" , err )
112
+ }
113
+
114
+ for _ , dc := range resp .Devcontainers {
115
+ if dc .Agent .ID == workspaceAgent .ID {
116
+ devcontainer = dc
117
+ break
118
+ }
119
+ }
120
+ if devcontainer .ID == uuid .Nil {
121
+ cliui .Warnf (inv .Stderr , "Devcontainer %q not found, opening as a regular workspace..." , workspaceAgent .Name )
122
+ parentWorkspaceAgent = codersdk.WorkspaceAgent {} // Reset to empty, so we don't use it later.
123
+ break
124
+ }
125
+
126
+ // Precondition, the devcontainer must be running to enter
127
+ // it. Once running, devcontainer.Container will be set.
128
+ if devcontainer .Status == codersdk .WorkspaceAgentDevcontainerStatusRunning {
129
+ break
130
+ }
131
+ if devcontainer .Status != codersdk .WorkspaceAgentDevcontainerStatusStarting {
132
+ return xerrors .Errorf ("devcontainer %q is in unexpected status %q, expected %q or %q" ,
133
+ devcontainer .Name , devcontainer .Status ,
134
+ codersdk .WorkspaceAgentDevcontainerStatusRunning ,
135
+ codersdk .WorkspaceAgentDevcontainerStatusStarting ,
136
+ )
137
+ }
138
+
139
+ if ! printedWaiting {
140
+ _ , _ = fmt .Fprintf (inv .Stderr , "Waiting for devcontainer %q status to change from %q to %q...\n " , devcontainer .Name , devcontainer .Status , codersdk .WorkspaceAgentDevcontainerStatusRunning )
141
+ printedWaiting = true
142
+ }
143
+ time .Sleep (5 * time .Second ) // Wait a bit before retrying.
144
+ }
145
+ }
146
+
82
147
if ! insideThisWorkspace {
83
148
// Wait for the agent to connect, we don't care about readiness
84
149
// otherwise (e.g. wait).
@@ -99,6 +164,9 @@ func (r *RootCmd) openVSCode() *serpent.Command {
99
164
// the created state, so we need to wait for that to happen.
100
165
// However, if no directory is set, the expanded directory will
101
166
// not be set either.
167
+ //
168
+ // Note that this is irrelevant for devcontainer sub agents, as
169
+ // they always have a directory set.
102
170
if workspaceAgent .Directory != "" {
103
171
workspace , workspaceAgent , err = waitForAgentCond (ctx , client , workspace , workspaceAgent , func (_ codersdk.WorkspaceAgent ) bool {
104
172
return workspaceAgent .LifecycleState != codersdk .WorkspaceAgentLifecycleCreated
@@ -114,41 +182,6 @@ func (r *RootCmd) openVSCode() *serpent.Command {
114
182
directory = inv .Args [1 ]
115
183
}
116
184
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
- }
122
-
123
- var foundContainer bool
124
-
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
185
directory , err = resolveAgentAbsPath (workspaceAgent .ExpandedDirectory , directory , workspaceAgent .OperatingSystem , insideThisWorkspace )
153
186
if err != nil {
154
187
return xerrors .Errorf ("resolve agent path: %w" , err )
@@ -174,14 +207,16 @@ func (r *RootCmd) openVSCode() *serpent.Command {
174
207
u * url.URL
175
208
qp url.Values
176
209
)
177
- if containerName != "" {
210
+ if devcontainer . ID != uuid . Nil {
178
211
u , qp = buildVSCodeWorkspaceDevContainerLink (
179
212
token ,
180
213
client .URL .String (),
181
214
workspace ,
182
- workspaceAgent ,
183
- containerName ,
215
+ parentWorkspaceAgent ,
216
+ devcontainer . Container . FriendlyName ,
184
217
directory ,
218
+ devcontainer .WorkspaceFolder ,
219
+ devcontainer .ConfigPath ,
185
220
)
186
221
} else {
187
222
u , qp = buildVSCodeWorkspaceLink (
@@ -247,13 +282,6 @@ func (r *RootCmd) openVSCode() *serpent.Command {
247
282
),
248
283
Value : serpent .BoolOf (& generateToken ),
249
284
},
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
- },
257
285
{
258
286
Flag : "test.open-error" ,
259
287
Description : "Don't run the open command." ,
@@ -288,7 +316,7 @@ func (r *RootCmd) openApp() *serpent.Command {
288
316
}
289
317
290
318
workspaceName := inv .Args [0 ]
291
- ws , agt , err := getWorkspaceAndAgent (ctx , inv , client , false , workspaceName )
319
+ ws , agt , _ , err := getWorkspaceAndAgent (ctx , inv , client , false , workspaceName )
292
320
if err != nil {
293
321
var sdkErr * codersdk.Error
294
322
if errors .As (err , & sdkErr ) && sdkErr .StatusCode () == http .StatusNotFound {
@@ -430,8 +458,14 @@ func buildVSCodeWorkspaceDevContainerLink(
430
458
workspaceAgent codersdk.WorkspaceAgent ,
431
459
containerName string ,
432
460
containerFolder string ,
461
+ localWorkspaceFolder string ,
462
+ localConfigFile string ,
433
463
) (* url.URL , url.Values ) {
434
464
containerFolder = filepath .ToSlash (containerFolder )
465
+ localWorkspaceFolder = filepath .ToSlash (localWorkspaceFolder )
466
+ if localConfigFile != "" {
467
+ localConfigFile = filepath .ToSlash (localConfigFile )
468
+ }
435
469
436
470
qp := url.Values {}
437
471
qp .Add ("url" , clientURL )
@@ -440,6 +474,8 @@ func buildVSCodeWorkspaceDevContainerLink(
440
474
qp .Add ("agent" , workspaceAgent .Name )
441
475
qp .Add ("devContainerName" , containerName )
442
476
qp .Add ("devContainerFolder" , containerFolder )
477
+ qp .Add ("localWorkspaceFolder" , localWorkspaceFolder )
478
+ qp .Add ("localConfigFile" , localConfigFile )
443
479
444
480
if token != "" {
445
481
qp .Add ("token" , token )
@@ -469,7 +505,7 @@ func waitForAgentCond(ctx context.Context, client *codersdk.Client, workspace co
469
505
}
470
506
471
507
for workspace = range wc {
472
- workspaceAgent , err = getWorkspaceAgent (workspace , workspaceAgent .Name )
508
+ workspaceAgent , _ , err = getWorkspaceAgent (workspace , workspaceAgent .Name )
473
509
if err != nil {
474
510
return workspace , workspaceAgent , xerrors .Errorf ("get workspace agent: %w" , err )
475
511
}
0 commit comments