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

Commit529fb50

Browse files
feat(agent/agentcontainers): support apps for dev container agents (#18346)
Add apps to the sub agent based on the dev container customization.The implementation also provides the following env variables for use inthe devcontainer json- `CODER_WORKSPACE_AGENT_NAME`- `CODER_WORKSPACE_USER_NAME`- `CODER_WORKSPACE_NAME`- `CODER_DEPLOYMENT_URL`
1 parent5e3a225 commit529fb50

File tree

9 files changed

+526
-20
lines changed

9 files changed

+526
-20
lines changed

‎agent/agentcontainers/acmock/acmock.go‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎agent/agentcontainers/api.go‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ type API struct {
6464
subAgentURLstring
6565
subAgentEnv []string
6666

67+
ownerNamestring
68+
workspaceNamestring
69+
6770
mu sync.RWMutex
6871
closedbool
6972
containers codersdk.WorkspaceAgentListContainersResponse// Output from the last list operation.
@@ -153,6 +156,15 @@ func WithSubAgentEnv(env ...string) Option {
153156
}
154157
}
155158

159+
// WithManifestInfo sets the owner name, and workspace name
160+
// for the sub-agent.
161+
funcWithManifestInfo(owner,workspacestring)Option {
162+
returnfunc(api*API) {
163+
api.ownerName=owner
164+
api.workspaceName=workspace
165+
}
166+
}
167+
156168
// WithDevcontainers sets the known devcontainers for the API. This
157169
// allows the API to be aware of devcontainers defined in the workspace
158170
// agent manifest.
@@ -1127,7 +1139,16 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
11271139
codersdk.DisplayAppPortForward:true,
11281140
}
11291141

1130-
ifconfig,err:=api.dccli.ReadConfig(ctx,dc.WorkspaceFolder,dc.ConfigPath);err!=nil {
1142+
varappsWithPossibleDuplicates []SubAgentApp
1143+
1144+
ifconfig,err:=api.dccli.ReadConfig(ctx,dc.WorkspaceFolder,dc.ConfigPath,
1145+
[]string{
1146+
fmt.Sprintf("CODER_WORKSPACE_AGENT_NAME=%s",dc.Name),
1147+
fmt.Sprintf("CODER_WORKSPACE_OWNER_NAME=%s",api.ownerName),
1148+
fmt.Sprintf("CODER_WORKSPACE_NAME=%s",api.workspaceName),
1149+
fmt.Sprintf("CODER_URL=%s",api.subAgentURL),
1150+
},
1151+
);err!=nil {
11311152
api.logger.Error(ctx,"unable to read devcontainer config",slog.Error(err))
11321153
}else {
11331154
coderCustomization:=config.MergedConfiguration.Customizations.Coder
@@ -1143,6 +1164,8 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
11431164
}
11441165
displayAppsMap[app]=enabled
11451166
}
1167+
1168+
appsWithPossibleDuplicates=append(appsWithPossibleDuplicates,customization.Apps...)
11461169
}
11471170
}
11481171

@@ -1154,7 +1177,27 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
11541177
}
11551178
slices.Sort(displayApps)
11561179

1180+
appSlugs:=make(map[string]struct{})
1181+
apps:=make([]SubAgentApp,0,len(appsWithPossibleDuplicates))
1182+
1183+
// We want to deduplicate the apps based on their slugs here.
1184+
// As we want to prioritize later apps, we will walk through this
1185+
// backwards.
1186+
for_,app:=rangeslices.Backward(appsWithPossibleDuplicates) {
1187+
if_,slugAlreadyExists:=appSlugs[app.Slug];slugAlreadyExists {
1188+
continue
1189+
}
1190+
1191+
appSlugs[app.Slug]=struct{}{}
1192+
apps=append(apps,app)
1193+
}
1194+
1195+
// Apps is currently in reverse order here, so by reversing it we restore
1196+
// it to the original order.
1197+
slices.Reverse(apps)
1198+
11571199
subAgentConfig.DisplayApps=displayApps
1200+
subAgentConfig.Apps=apps
11581201
}
11591202

11601203
deleteSubAgent:=proc.agent.ID!=uuid.Nil&&maybeRecreateSubAgent&&!proc.agent.EqualConfig(subAgentConfig)

‎agent/agentcontainers/api_test.go‎

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ type fakeDevcontainerCLI struct {
6868
execErrCchanfunc(cmdstring,args...string)error// If set, send fn to return err, nil or close to return execErr.
6969
readConfig agentcontainers.DevcontainerConfig
7070
readConfigErrerror
71-
readConfigErrCchanerror
71+
readConfigErrCchanfunc(envs []string)error
7272
}
7373

7474
func (f*fakeDevcontainerCLI)Up(ctx context.Context,_,_string,_...agentcontainers.DevcontainerCLIUpOptions) (string,error) {
@@ -99,14 +99,14 @@ func (f *fakeDevcontainerCLI) Exec(ctx context.Context, _, _ string, cmd string,
9999
returnf.execErr
100100
}
101101

102-
func (f*fakeDevcontainerCLI)ReadConfig(ctx context.Context,_,_string,_...agentcontainers.DevcontainerCLIReadConfigOptions) (agentcontainers.DevcontainerConfig,error) {
102+
func (f*fakeDevcontainerCLI)ReadConfig(ctx context.Context,_,_string,envs []string,_...agentcontainers.DevcontainerCLIReadConfigOptions) (agentcontainers.DevcontainerConfig,error) {
103103
iff.readConfigErrC!=nil {
104104
select {
105105
case<-ctx.Done():
106106
return agentcontainers.DevcontainerConfig{},ctx.Err()
107-
caseerr,ok:=<-f.readConfigErrC:
107+
casefn,ok:=<-f.readConfigErrC:
108108
ifok {
109-
returnf.readConfig,err
109+
returnf.readConfig,fn(envs)
110110
}
111111
}
112112
}
@@ -1253,7 +1253,8 @@ func TestAPI(t *testing.T) {
12531253
deleteErrC:make(chanerror,1),
12541254
}
12551255
fakeDCCLI=&fakeDevcontainerCLI{
1256-
execErrC:make(chanfunc(cmdstring,args...string)error,1),
1256+
execErrC:make(chanfunc(cmdstring,args...string)error,1),
1257+
readConfigErrC:make(chanfunc(envs []string)error,1),
12571258
}
12581259

12591260
testContainer= codersdk.WorkspaceAgentContainer{
@@ -1293,13 +1294,15 @@ func TestAPI(t *testing.T) {
12931294
agentcontainers.WithSubAgentClient(fakeSAC),
12941295
agentcontainers.WithSubAgentURL("test-subagent-url"),
12951296
agentcontainers.WithDevcontainerCLI(fakeDCCLI),
1297+
agentcontainers.WithManifestInfo("test-user","test-workspace"),
12961298
)
12971299
apiClose:=func() {
12981300
closeOnce.Do(func() {
12991301
// Close before api.Close() defer to avoid deadlock after test.
13001302
close(fakeSAC.createErrC)
13011303
close(fakeSAC.deleteErrC)
13021304
close(fakeDCCLI.execErrC)
1305+
close(fakeDCCLI.readConfigErrC)
13031306

13041307
_=api.Close()
13051308
})
@@ -1313,6 +1316,13 @@ func TestAPI(t *testing.T) {
13131316
assert.Empty(t,args)
13141317
returnnil
13151318
})// Exec pwd.
1319+
testutil.RequireSend(ctx,t,fakeDCCLI.readConfigErrC,func(envs []string)error {
1320+
assert.Contains(t,envs,"CODER_WORKSPACE_AGENT_NAME=test-container")
1321+
assert.Contains(t,envs,"CODER_WORKSPACE_NAME=test-workspace")
1322+
assert.Contains(t,envs,"CODER_WORKSPACE_OWNER_NAME=test-user")
1323+
assert.Contains(t,envs,"CODER_URL=test-subagent-url")
1324+
returnnil
1325+
})
13161326

13171327
// Make sure the ticker function has been registered
13181328
// before advancing the clock.
@@ -1453,6 +1463,13 @@ func TestAPI(t *testing.T) {
14531463
assert.Empty(t,args)
14541464
returnnil
14551465
})// Exec pwd.
1466+
testutil.RequireSend(ctx,t,fakeDCCLI.readConfigErrC,func(envs []string)error {
1467+
assert.Contains(t,envs,"CODER_WORKSPACE_AGENT_NAME=test-container")
1468+
assert.Contains(t,envs,"CODER_WORKSPACE_NAME=test-workspace")
1469+
assert.Contains(t,envs,"CODER_WORKSPACE_OWNER_NAME=test-user")
1470+
assert.Contains(t,envs,"CODER_URL=test-subagent-url")
1471+
returnnil
1472+
})
14561473

14571474
err=api.RefreshContainers(ctx)
14581475
require.NoError(t,err,"refresh containers should not fail")
@@ -1603,6 +1620,116 @@ func TestAPI(t *testing.T) {
16031620
assert.Contains(t,subAgent.DisplayApps,codersdk.DisplayAppPortForward)
16041621
},
16051622
},
1623+
{
1624+
name:"WithApps",
1625+
customization: []agentcontainers.CoderCustomization{
1626+
{
1627+
Apps: []agentcontainers.SubAgentApp{
1628+
{
1629+
Slug:"web-app",
1630+
DisplayName:"Web Application",
1631+
URL:"http://localhost:8080",
1632+
OpenIn:codersdk.WorkspaceAppOpenInTab,
1633+
Share:codersdk.WorkspaceAppSharingLevelOwner,
1634+
Icon:"/icons/web.svg",
1635+
Order:int32(1),
1636+
},
1637+
{
1638+
Slug:"api-server",
1639+
DisplayName:"API Server",
1640+
URL:"http://localhost:3000",
1641+
OpenIn:codersdk.WorkspaceAppOpenInSlimWindow,
1642+
Share:codersdk.WorkspaceAppSharingLevelAuthenticated,
1643+
Icon:"/icons/api.svg",
1644+
Order:int32(2),
1645+
Hidden:true,
1646+
},
1647+
{
1648+
Slug:"docs",
1649+
DisplayName:"Documentation",
1650+
URL:"http://localhost:4000",
1651+
OpenIn:codersdk.WorkspaceAppOpenInTab,
1652+
Share:codersdk.WorkspaceAppSharingLevelPublic,
1653+
Icon:"/icons/book.svg",
1654+
Order:int32(3),
1655+
},
1656+
},
1657+
},
1658+
},
1659+
afterCreate:func(t*testing.T,subAgent agentcontainers.SubAgent) {
1660+
require.Len(t,subAgent.Apps,3)
1661+
1662+
// Verify first app
1663+
assert.Equal(t,"web-app",subAgent.Apps[0].Slug)
1664+
assert.Equal(t,"Web Application",subAgent.Apps[0].DisplayName)
1665+
assert.Equal(t,"http://localhost:8080",subAgent.Apps[0].URL)
1666+
assert.Equal(t,codersdk.WorkspaceAppOpenInTab,subAgent.Apps[0].OpenIn)
1667+
assert.Equal(t,codersdk.WorkspaceAppSharingLevelOwner,subAgent.Apps[0].Share)
1668+
assert.Equal(t,"/icons/web.svg",subAgent.Apps[0].Icon)
1669+
assert.Equal(t,int32(1),subAgent.Apps[0].Order)
1670+
1671+
// Verify second app
1672+
assert.Equal(t,"api-server",subAgent.Apps[1].Slug)
1673+
assert.Equal(t,"API Server",subAgent.Apps[1].DisplayName)
1674+
assert.Equal(t,"http://localhost:3000",subAgent.Apps[1].URL)
1675+
assert.Equal(t,codersdk.WorkspaceAppOpenInSlimWindow,subAgent.Apps[1].OpenIn)
1676+
assert.Equal(t,codersdk.WorkspaceAppSharingLevelAuthenticated,subAgent.Apps[1].Share)
1677+
assert.Equal(t,"/icons/api.svg",subAgent.Apps[1].Icon)
1678+
assert.Equal(t,int32(2),subAgent.Apps[1].Order)
1679+
assert.Equal(t,true,subAgent.Apps[1].Hidden)
1680+
1681+
// Verify third app
1682+
assert.Equal(t,"docs",subAgent.Apps[2].Slug)
1683+
assert.Equal(t,"Documentation",subAgent.Apps[2].DisplayName)
1684+
assert.Equal(t,"http://localhost:4000",subAgent.Apps[2].URL)
1685+
assert.Equal(t,codersdk.WorkspaceAppOpenInTab,subAgent.Apps[2].OpenIn)
1686+
assert.Equal(t,codersdk.WorkspaceAppSharingLevelPublic,subAgent.Apps[2].Share)
1687+
assert.Equal(t,"/icons/book.svg",subAgent.Apps[2].Icon)
1688+
assert.Equal(t,int32(3),subAgent.Apps[2].Order)
1689+
},
1690+
},
1691+
{
1692+
name:"AppDeduplication",
1693+
customization: []agentcontainers.CoderCustomization{
1694+
{
1695+
Apps: []agentcontainers.SubAgentApp{
1696+
{
1697+
Slug:"foo-app",
1698+
Hidden:true,
1699+
Order:1,
1700+
},
1701+
{
1702+
Slug:"bar-app",
1703+
},
1704+
},
1705+
},
1706+
{
1707+
Apps: []agentcontainers.SubAgentApp{
1708+
{
1709+
Slug:"foo-app",
1710+
Order:2,
1711+
},
1712+
{
1713+
Slug:"baz-app",
1714+
},
1715+
},
1716+
},
1717+
},
1718+
afterCreate:func(t*testing.T,subAgent agentcontainers.SubAgent) {
1719+
require.Len(t,subAgent.Apps,3)
1720+
1721+
// As the original "foo-app" gets overridden by the later "foo-app",
1722+
// we expect "bar-app" to be first in the order.
1723+
assert.Equal(t,"bar-app",subAgent.Apps[0].Slug)
1724+
assert.Equal(t,"foo-app",subAgent.Apps[1].Slug)
1725+
assert.Equal(t,"baz-app",subAgent.Apps[2].Slug)
1726+
1727+
// We do not expect the properties from the original "foo-app" to be
1728+
// carried over.
1729+
assert.Equal(t,false,subAgent.Apps[1].Hidden)
1730+
assert.Equal(t,int32(2),subAgent.Apps[1].Order)
1731+
},
1732+
},
16061733
}
16071734

16081735
for_,tt:=rangetests {

‎agent/agentcontainers/devcontainercli.go‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/json"
88
"errors"
99
"io"
10+
"os"
1011

1112
"golang.org/x/xerrors"
1213

@@ -32,13 +33,14 @@ type DevcontainerCustomizations struct {
3233

3334
typeCoderCustomizationstruct {
3435
DisplayAppsmap[codersdk.DisplayApp]bool`json:"displayApps,omitempty"`
36+
Apps []SubAgentApp`json:"apps,omitempty"`
3537
}
3638

3739
// DevcontainerCLI is an interface for the devcontainer CLI.
3840
typeDevcontainerCLIinterface {
3941
Up(ctx context.Context,workspaceFolder,configPathstring,opts...DevcontainerCLIUpOptions) (idstring,errerror)
4042
Exec(ctx context.Context,workspaceFolder,configPathstring,cmdstring,cmdArgs []string,opts...DevcontainerCLIExecOptions)error
41-
ReadConfig(ctx context.Context,workspaceFolder,configPathstring,opts...DevcontainerCLIReadConfigOptions) (DevcontainerConfig,error)
43+
ReadConfig(ctx context.Context,workspaceFolder,configPathstring,env []string,opts...DevcontainerCLIReadConfigOptions) (DevcontainerConfig,error)
4244
}
4345

4446
// DevcontainerCLIUpOptions are options for the devcontainer CLI Up
@@ -113,8 +115,8 @@ type devcontainerCLIReadConfigConfig struct {
113115
stderr io.Writer
114116
}
115117

116-
//WithExecOutput sets additional stdout and stderr writers for logs
117-
// duringExec operations.
118+
//WithReadConfigOutput sets additional stdout and stderr writers for logs
119+
// duringReadConfig operations.
118120
funcWithReadConfigOutput(stdout,stderr io.Writer)DevcontainerCLIReadConfigOptions {
119121
returnfunc(o*devcontainerCLIReadConfigConfig) {
120122
o.stdout=stdout
@@ -250,7 +252,7 @@ func (d *devcontainerCLI) Exec(ctx context.Context, workspaceFolder, configPath
250252
returnnil
251253
}
252254

253-
func (d*devcontainerCLI)ReadConfig(ctx context.Context,workspaceFolder,configPathstring,opts...DevcontainerCLIReadConfigOptions) (DevcontainerConfig,error) {
255+
func (d*devcontainerCLI)ReadConfig(ctx context.Context,workspaceFolder,configPathstring,env []string,opts...DevcontainerCLIReadConfigOptions) (DevcontainerConfig,error) {
254256
conf:=applyDevcontainerCLIReadConfigOptions(opts)
255257
logger:=d.logger.With(slog.F("workspace_folder",workspaceFolder),slog.F("config_path",configPath))
256258

@@ -263,6 +265,8 @@ func (d *devcontainerCLI) ReadConfig(ctx context.Context, workspaceFolder, confi
263265
}
264266

265267
c:=d.execer.CommandContext(ctx,"devcontainer",args...)
268+
c.Env=append(c.Env,"PATH="+os.Getenv("PATH"))
269+
c.Env=append(c.Env,env...)
266270

267271
varstdoutBuf bytes.Buffer
268272
stdoutWriters:= []io.Writer{&stdoutBuf,&devcontainerCLILogWriter{ctx:ctx,logger:logger.With(slog.F("stdout",true))}}

‎agent/agentcontainers/devcontainercli_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ func TestDevcontainerCLI_ArgsAndParsing(t *testing.T) {
316316
}
317317

318318
dccli:=agentcontainers.NewDevcontainerCLI(logger,testExecer)
319-
config,err:=dccli.ReadConfig(ctx,tt.workspaceFolder,tt.configPath,tt.opts...)
319+
config,err:=dccli.ReadConfig(ctx,tt.workspaceFolder,tt.configPath,[]string{},tt.opts...)
320320
iftt.wantError {
321321
assert.Error(t,err,"want error")
322322
assert.Equal(t, agentcontainers.DevcontainerConfig{},config,"expected empty config on error")

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp