Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit6c4db7a

Browse files
authored
feat(cli): replace open vscode container with devcontainer subagent (#18765)
This change allows a devcontainer to be opened via the agent syntax,`coder open vscode <workspace>.<agent>` and removes the `--container`option to simplify the subcommand. Accessing the subagent will behavesimilarly to how the `--container` option behaved.Fixescoder/internal#748
1 parent5f50dcc commit6c4db7a

File tree

10 files changed

+283
-323
lines changed

10 files changed

+283
-323
lines changed

‎agent/agentcontainers/devcontainercli.go

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -106,63 +106,63 @@ type DevcontainerCLI interface {
106106

107107
// DevcontainerCLIUpOptions are options for the devcontainer CLI Up
108108
// command.
109-
typeDevcontainerCLIUpOptionsfunc(*devcontainerCLIUpConfig)
109+
typeDevcontainerCLIUpOptionsfunc(*DevcontainerCLIUpConfig)
110110

111-
typedevcontainerCLIUpConfigstruct {
112-
args []string// Additional arguments for the Up command.
113-
stdout io.Writer
114-
stderr io.Writer
111+
typeDevcontainerCLIUpConfigstruct {
112+
Args []string// Additional arguments for the Up command.
113+
Stdout io.Writer
114+
Stderr io.Writer
115115
}
116116

117117
// WithRemoveExistingContainer is an option to remove the existing
118118
// container.
119119
funcWithRemoveExistingContainer()DevcontainerCLIUpOptions {
120-
returnfunc(o*devcontainerCLIUpConfig) {
121-
o.args=append(o.args,"--remove-existing-container")
120+
returnfunc(o*DevcontainerCLIUpConfig) {
121+
o.Args=append(o.Args,"--remove-existing-container")
122122
}
123123
}
124124

125125
// WithUpOutput sets additional stdout and stderr writers for logs
126126
// during Up operations.
127127
funcWithUpOutput(stdout,stderr io.Writer)DevcontainerCLIUpOptions {
128-
returnfunc(o*devcontainerCLIUpConfig) {
129-
o.stdout=stdout
130-
o.stderr=stderr
128+
returnfunc(o*DevcontainerCLIUpConfig) {
129+
o.Stdout=stdout
130+
o.Stderr=stderr
131131
}
132132
}
133133

134134
// DevcontainerCLIExecOptions are options for the devcontainer CLI Exec
135135
// command.
136-
typeDevcontainerCLIExecOptionsfunc(*devcontainerCLIExecConfig)
136+
typeDevcontainerCLIExecOptionsfunc(*DevcontainerCLIExecConfig)
137137

138-
typedevcontainerCLIExecConfigstruct {
139-
args []string// Additional arguments for the Exec command.
140-
stdout io.Writer
141-
stderr io.Writer
138+
typeDevcontainerCLIExecConfigstruct {
139+
Args []string// Additional arguments for the Exec command.
140+
Stdout io.Writer
141+
Stderr io.Writer
142142
}
143143

144144
// WithExecOutput sets additional stdout and stderr writers for logs
145145
// during Exec operations.
146146
funcWithExecOutput(stdout,stderr io.Writer)DevcontainerCLIExecOptions {
147-
returnfunc(o*devcontainerCLIExecConfig) {
148-
o.stdout=stdout
149-
o.stderr=stderr
147+
returnfunc(o*DevcontainerCLIExecConfig) {
148+
o.Stdout=stdout
149+
o.Stderr=stderr
150150
}
151151
}
152152

153153
// WithExecContainerID sets the container ID to target a specific
154154
// container.
155155
funcWithExecContainerID(idstring)DevcontainerCLIExecOptions {
156-
returnfunc(o*devcontainerCLIExecConfig) {
157-
o.args=append(o.args,"--container-id",id)
156+
returnfunc(o*DevcontainerCLIExecConfig) {
157+
o.Args=append(o.Args,"--container-id",id)
158158
}
159159
}
160160

161161
// WithRemoteEnv sets environment variables for the Exec command.
162162
funcWithRemoteEnv(env...string)DevcontainerCLIExecOptions {
163-
returnfunc(o*devcontainerCLIExecConfig) {
163+
returnfunc(o*DevcontainerCLIExecConfig) {
164164
for_,e:=rangeenv {
165-
o.args=append(o.args,"--remote-env",e)
165+
o.Args=append(o.Args,"--remote-env",e)
166166
}
167167
}
168168
}
@@ -185,8 +185,8 @@ func WithReadConfigOutput(stdout, stderr io.Writer) DevcontainerCLIReadConfigOpt
185185
}
186186
}
187187

188-
funcapplyDevcontainerCLIUpOptions(opts []DevcontainerCLIUpOptions)devcontainerCLIUpConfig {
189-
conf:=devcontainerCLIUpConfig{stdout:io.Discard,stderr:io.Discard}
188+
funcapplyDevcontainerCLIUpOptions(opts []DevcontainerCLIUpOptions)DevcontainerCLIUpConfig {
189+
conf:=DevcontainerCLIUpConfig{Stdout:io.Discard,Stderr:io.Discard}
190190
for_,opt:=rangeopts {
191191
ifopt!=nil {
192192
opt(&conf)
@@ -195,8 +195,8 @@ func applyDevcontainerCLIUpOptions(opts []DevcontainerCLIUpOptions) devcontainer
195195
returnconf
196196
}
197197

198-
funcapplyDevcontainerCLIExecOptions(opts []DevcontainerCLIExecOptions)devcontainerCLIExecConfig {
199-
conf:=devcontainerCLIExecConfig{stdout:io.Discard,stderr:io.Discard}
198+
funcapplyDevcontainerCLIExecOptions(opts []DevcontainerCLIExecOptions)DevcontainerCLIExecConfig {
199+
conf:=DevcontainerCLIExecConfig{Stdout:io.Discard,Stderr:io.Discard}
200200
for_,opt:=rangeopts {
201201
ifopt!=nil {
202202
opt(&conf)
@@ -241,7 +241,7 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
241241
ifconfigPath!="" {
242242
args=append(args,"--config",configPath)
243243
}
244-
args=append(args,conf.args...)
244+
args=append(args,conf.Args...)
245245
cmd:=d.execer.CommandContext(ctx,"devcontainer",args...)
246246

247247
// Capture stdout for parsing and stream logs for both default and provided writers.
@@ -251,14 +251,14 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
251251
&devcontainerCLILogWriter{
252252
ctx:ctx,
253253
logger:logger.With(slog.F("stdout",true)),
254-
writer:conf.stdout,
254+
writer:conf.Stdout,
255255
},
256256
)
257257
// Stream stderr logs and provided writer if any.
258258
cmd.Stderr=&devcontainerCLILogWriter{
259259
ctx:ctx,
260260
logger:logger.With(slog.F("stderr",true)),
261-
writer:conf.stderr,
261+
writer:conf.Stderr,
262262
}
263263

264264
iferr:=cmd.Run();err!=nil {
@@ -293,17 +293,17 @@ func (d *devcontainerCLI) Exec(ctx context.Context, workspaceFolder, configPath
293293
ifconfigPath!="" {
294294
args=append(args,"--config",configPath)
295295
}
296-
args=append(args,conf.args...)
296+
args=append(args,conf.Args...)
297297
args=append(args,cmd)
298298
args=append(args,cmdArgs...)
299299
c:=d.execer.CommandContext(ctx,"devcontainer",args...)
300300

301-
c.Stdout=io.MultiWriter(conf.stdout,&devcontainerCLILogWriter{
301+
c.Stdout=io.MultiWriter(conf.Stdout,&devcontainerCLILogWriter{
302302
ctx:ctx,
303303
logger:logger.With(slog.F("stdout",true)),
304304
writer:io.Discard,
305305
})
306-
c.Stderr=io.MultiWriter(conf.stderr,&devcontainerCLILogWriter{
306+
c.Stderr=io.MultiWriter(conf.Stderr,&devcontainerCLILogWriter{
307307
ctx:ctx,
308308
logger:logger.With(slog.F("stderr",true)),
309309
writer:io.Discard,

‎cli/exp_rpty.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func handleRPTY(inv *serpent.Invocation, client *codersdk.Client, args handleRPT
9797
reconnectID=uuid.New()
9898
}
9999

100-
ws,agt,err:=getWorkspaceAndAgent(ctx,inv,client,true,args.NamedWorkspace)
100+
ws,agt,_,err:=getWorkspaceAndAgent(ctx,inv,client,true,args.NamedWorkspace)
101101
iferr!=nil {
102102
returnerr
103103
}

‎cli/open.go

Lines changed: 85 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import (
1111
"runtime"
1212
"slices"
1313
"strings"
14+
"time"
1415

16+
"github.com/google/uuid"
1517
"github.com/skratchdot/open-golang/open"
1618
"golang.org/x/xerrors"
1719

@@ -42,7 +44,6 @@ func (r *RootCmd) openVSCode() *serpent.Command {
4244
generateTokenbool
4345
testOpenErrorbool
4446
appearanceConfig codersdk.AppearanceConfig
45-
containerNamestring
4647
)
4748

4849
client:=new(codersdk.Client)
@@ -71,14 +72,78 @@ func (r *RootCmd) openVSCode() *serpent.Command {
7172
// need to wait for the agent to start.
7273
workspaceQuery:=inv.Args[0]
7374
autostart:=true
74-
workspace,workspaceAgent,err:=getWorkspaceAndAgent(ctx,inv,client,autostart,workspaceQuery)
75+
workspace,workspaceAgent,otherWorkspaceAgents,err:=getWorkspaceAndAgent(ctx,inv,client,autostart,workspaceQuery)
7576
iferr!=nil {
7677
returnxerrors.Errorf("get workspace and agent: %w",err)
7778
}
7879

7980
workspaceName:=workspace.Name+"."+workspaceAgent.Name
8081
insideThisWorkspace:=insideAWorkspace&&inWorkspaceName==workspaceName
8182

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+
varparentWorkspaceAgent codersdk.WorkspaceAgent
93+
vardevcontainer codersdk.WorkspaceAgentDevcontainer
94+
ifworkspaceAgent.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:=rangeotherWorkspaceAgents {
98+
ifotherAgent.ID==workspaceAgent.ParentID.UUID {
99+
parentWorkspaceAgent=otherAgent
100+
break
101+
}
102+
}
103+
ifparentWorkspaceAgent.ID==uuid.Nil {
104+
returnxerrors.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+
iferr!=nil {
111+
returnxerrors.Errorf("list parent workspace agent containers: %w",err)
112+
}
113+
114+
for_,dc:=rangeresp.Devcontainers {
115+
ifdc.Agent.ID==workspaceAgent.ID {
116+
devcontainer=dc
117+
break
118+
}
119+
}
120+
ifdevcontainer.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+
ifdevcontainer.Status==codersdk.WorkspaceAgentDevcontainerStatusRunning {
129+
break
130+
}
131+
ifdevcontainer.Status!=codersdk.WorkspaceAgentDevcontainerStatusStarting {
132+
returnxerrors.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+
82147
if!insideThisWorkspace {
83148
// Wait for the agent to connect, we don't care about readiness
84149
// otherwise (e.g. wait).
@@ -99,6 +164,9 @@ func (r *RootCmd) openVSCode() *serpent.Command {
99164
// the created state, so we need to wait for that to happen.
100165
// However, if no directory is set, the expanded directory will
101166
// not be set either.
167+
//
168+
// Note that this is irrelevant for devcontainer sub agents, as
169+
// they always have a directory set.
102170
ifworkspaceAgent.Directory!="" {
103171
workspace,workspaceAgent,err=waitForAgentCond(ctx,client,workspace,workspaceAgent,func(_ codersdk.WorkspaceAgent)bool {
104172
returnworkspaceAgent.LifecycleState!=codersdk.WorkspaceAgentLifecycleCreated
@@ -114,41 +182,6 @@ func (r *RootCmd) openVSCode() *serpent.Command {
114182
directory=inv.Args[1]
115183
}
116184

117-
ifcontainerName!="" {
118-
containers,err:=client.WorkspaceAgentListContainers(ctx,workspaceAgent.ID,map[string]string{"devcontainer.local_folder":""})
119-
iferr!=nil {
120-
returnxerrors.Errorf("list workspace agent containers: %w",err)
121-
}
122-
123-
varfoundContainerbool
124-
125-
for_,container:=rangecontainers.Containers {
126-
ifcontainer.FriendlyName!=containerName {
127-
continue
128-
}
129-
130-
foundContainer=true
131-
132-
ifdirectory=="" {
133-
localFolder,ok:=container.Labels["devcontainer.local_folder"]
134-
if!ok {
135-
returnxerrors.New("container missing `devcontainer.local_folder` label")
136-
}
137-
138-
directory,ok=container.Volumes[localFolder]
139-
if!ok {
140-
returnxerrors.New("container missing volume for `devcontainer.local_folder`")
141-
}
142-
}
143-
144-
break
145-
}
146-
147-
if!foundContainer {
148-
returnxerrors.New("no container found")
149-
}
150-
}
151-
152185
directory,err=resolveAgentAbsPath(workspaceAgent.ExpandedDirectory,directory,workspaceAgent.OperatingSystem,insideThisWorkspace)
153186
iferr!=nil {
154187
returnxerrors.Errorf("resolve agent path: %w",err)
@@ -174,14 +207,16 @@ func (r *RootCmd) openVSCode() *serpent.Command {
174207
u*url.URL
175208
qp url.Values
176209
)
177-
ifcontainerName!="" {
210+
ifdevcontainer.ID!=uuid.Nil {
178211
u,qp=buildVSCodeWorkspaceDevContainerLink(
179212
token,
180213
client.URL.String(),
181214
workspace,
182-
workspaceAgent,
183-
containerName,
215+
parentWorkspaceAgent,
216+
devcontainer.Container.FriendlyName,
184217
directory,
218+
devcontainer.WorkspaceFolder,
219+
devcontainer.ConfigPath,
185220
)
186221
}else {
187222
u,qp=buildVSCodeWorkspaceLink(
@@ -247,13 +282,6 @@ func (r *RootCmd) openVSCode() *serpent.Command {
247282
),
248283
Value:serpent.BoolOf(&generateToken),
249284
},
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-
},
257285
{
258286
Flag:"test.open-error",
259287
Description:"Don't run the open command.",
@@ -288,7 +316,7 @@ func (r *RootCmd) openApp() *serpent.Command {
288316
}
289317

290318
workspaceName:=inv.Args[0]
291-
ws,agt,err:=getWorkspaceAndAgent(ctx,inv,client,false,workspaceName)
319+
ws,agt,_,err:=getWorkspaceAndAgent(ctx,inv,client,false,workspaceName)
292320
iferr!=nil {
293321
varsdkErr*codersdk.Error
294322
iferrors.As(err,&sdkErr)&&sdkErr.StatusCode()==http.StatusNotFound {
@@ -430,8 +458,14 @@ func buildVSCodeWorkspaceDevContainerLink(
430458
workspaceAgent codersdk.WorkspaceAgent,
431459
containerNamestring,
432460
containerFolderstring,
461+
localWorkspaceFolderstring,
462+
localConfigFilestring,
433463
) (*url.URL, url.Values) {
434464
containerFolder=filepath.ToSlash(containerFolder)
465+
localWorkspaceFolder=filepath.ToSlash(localWorkspaceFolder)
466+
iflocalConfigFile!="" {
467+
localConfigFile=filepath.ToSlash(localConfigFile)
468+
}
435469

436470
qp:= url.Values{}
437471
qp.Add("url",clientURL)
@@ -440,6 +474,8 @@ func buildVSCodeWorkspaceDevContainerLink(
440474
qp.Add("agent",workspaceAgent.Name)
441475
qp.Add("devContainerName",containerName)
442476
qp.Add("devContainerFolder",containerFolder)
477+
qp.Add("localWorkspaceFolder",localWorkspaceFolder)
478+
qp.Add("localConfigFile",localConfigFile)
443479

444480
iftoken!="" {
445481
qp.Add("token",token)
@@ -469,7 +505,7 @@ func waitForAgentCond(ctx context.Context, client *codersdk.Client, workspace co
469505
}
470506

471507
forworkspace=rangewc {
472-
workspaceAgent,err=getWorkspaceAgent(workspace,workspaceAgent.Name)
508+
workspaceAgent,_,err=getWorkspaceAgent(workspace,workspaceAgent.Name)
473509
iferr!=nil {
474510
returnworkspace,workspaceAgent,xerrors.Errorf("get workspace agent: %w",err)
475511
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp