Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 08f0f9c

Browse files
feat(agent/agentcontainers): support apps for dev container agents
1 parent 4b6209e commit 08f0f9c

File tree

8 files changed

+461
-16
lines changed

8 files changed

+461
-16
lines changed

agent/agentcontainers/acmock/acmock.go

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

agent/agentcontainers/api.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ type API struct {
6363
subAgentURL string
6464
subAgentEnv []string
6565

66+
userName string
67+
workspaceName string
68+
6669
mu sync.RWMutex
6770
closed bool
6871
containers codersdk.WorkspaceAgentListContainersResponse // Output from the last list operation.
@@ -151,6 +154,20 @@ func WithSubAgentEnv(env ...string) Option {
151154
}
152155
}
153156

157+
// WithWorkspaceName sets the workspace name for the sub-agent.
158+
func WithWorkspaceName(name string) Option {
159+
return func(api *API) {
160+
api.workspaceName = name
161+
}
162+
}
163+
164+
// WithUserName sets the workspace name for the sub-agent.
165+
func WithUserName(name string) Option {
166+
return func(api *API) {
167+
api.userName = name
168+
}
169+
}
170+
154171
// WithDevcontainers sets the known devcontainers for the API. This
155172
// allows the API to be aware of devcontainers defined in the workspace
156173
// agent manifest.
@@ -1100,13 +1117,24 @@ func (api *API) injectSubAgentIntoContainerLocked(ctx context.Context, dc coders
11001117
}
11011118

11021119
var displayApps []codersdk.DisplayApp
1103-
1104-
if config, err := api.dccli.ReadConfig(ctx, dc.WorkspaceFolder, dc.ConfigPath); err != nil {
1120+
var apps []SubAgentApp
1121+
1122+
if config, err := api.dccli.ReadConfig(ctx,
1123+
dc.WorkspaceFolder,
1124+
dc.ConfigPath,
1125+
[]string{
1126+
fmt.Sprintf("CODER_AGENT_NAME=%s", dc.Name),
1127+
fmt.Sprintf("CODER_USER_NAME=%s", api.userName),
1128+
fmt.Sprintf("CODER_WORKSPACE_NAME=%s", api.workspaceName),
1129+
fmt.Sprintf("CODER_DEPLOYMENT_URL=%s", api.subAgentURL),
1130+
},
1131+
); err != nil {
11051132
api.logger.Error(ctx, "unable to read devcontainer config", slog.Error(err))
11061133
} else {
11071134
coderCustomization := config.Configuration.Customizations.Coder
11081135
if coderCustomization != nil {
11091136
displayApps = coderCustomization.DisplayApps
1137+
apps = coderCustomization.Apps
11101138
}
11111139
}
11121140

@@ -1118,6 +1146,7 @@ func (api *API) injectSubAgentIntoContainerLocked(ctx context.Context, dc coders
11181146
OperatingSystem: "linux", // Assuming Linux for dev containers.
11191147
Architecture: arch,
11201148
DisplayApps: displayApps,
1149+
Apps: apps,
11211150
})
11221151
if err != nil {
11231152
return xerrors.Errorf("create agent: %w", err)

agent/agentcontainers/api_test.go

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/coder/coder/v2/agent/agentcontainers"
2626
"github.com/coder/coder/v2/agent/agentcontainers/acmock"
2727
"github.com/coder/coder/v2/agent/agentcontainers/watcher"
28+
"github.com/coder/coder/v2/coderd/util/ptr"
2829
"github.com/coder/coder/v2/codersdk"
2930
"github.com/coder/coder/v2/testutil"
3031
"github.com/coder/quartz"
@@ -67,7 +68,7 @@ type fakeDevcontainerCLI struct {
6768
execErrC chan func(cmd string, args ...string) error // If set, send fn to return err, nil or close to return execErr.
6869
readConfig agentcontainers.DevcontainerConfig
6970
readConfigErr error
70-
readConfigErrC chan error
71+
readConfigErrC chan func(envs []string) (agentcontainers.DevcontainerConfig, error)
7172
}
7273

7374
func (f *fakeDevcontainerCLI) Up(ctx context.Context, _, _ string, _ ...agentcontainers.DevcontainerCLIUpOptions) (string, error) {
@@ -98,14 +99,14 @@ func (f *fakeDevcontainerCLI) Exec(ctx context.Context, _, _ string, cmd string,
9899
return f.execErr
99100
}
100101

101-
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) {
102103
if f.readConfigErrC != nil {
103104
select {
104105
case <-ctx.Done():
105106
return agentcontainers.DevcontainerConfig{}, ctx.Err()
106-
case err, ok := <-f.readConfigErrC:
107+
case fn, ok := <-f.readConfigErrC:
107108
if ok {
108-
return f.readConfig, err
109+
return fn(envs)
109110
}
110111
}
111112
}
@@ -1268,7 +1269,8 @@ func TestAPI(t *testing.T) {
12681269
deleteErrC: make(chan error, 1),
12691270
}
12701271
fakeDCCLI = &fakeDevcontainerCLI{
1271-
execErrC: make(chan func(cmd string, args ...string) error, 1),
1272+
execErrC: make(chan func(cmd string, args ...string) error, 1),
1273+
readConfigErrC: make(chan func(envs []string) (agentcontainers.DevcontainerConfig, error), 1),
12721274
}
12731275

12741276
testContainer = codersdk.WorkspaceAgentContainer{
@@ -1307,13 +1309,16 @@ func TestAPI(t *testing.T) {
13071309
agentcontainers.WithSubAgentClient(fakeSAC),
13081310
agentcontainers.WithSubAgentURL("test-subagent-url"),
13091311
agentcontainers.WithDevcontainerCLI(fakeDCCLI),
1312+
agentcontainers.WithUserName("test-user"),
1313+
agentcontainers.WithWorkspaceName("test-workspace"),
13101314
)
13111315
defer api.Close()
13121316

13131317
// Close before api.Close() defer to avoid deadlock after test.
13141318
defer close(fakeSAC.createErrC)
13151319
defer close(fakeSAC.deleteErrC)
13161320
defer close(fakeDCCLI.execErrC)
1321+
defer close(fakeDCCLI.readConfigErrC)
13171322

13181323
// Allow initial agent creation and injection to succeed.
13191324
testutil.RequireSend(ctx, t, fakeSAC.createErrC, nil)
@@ -1322,6 +1327,13 @@ func TestAPI(t *testing.T) {
13221327
assert.Empty(t, args)
13231328
return nil
13241329
}) // Exec pwd.
1330+
testutil.RequireSend(ctx, t, fakeDCCLI.readConfigErrC, func(envs []string) (agentcontainers.DevcontainerConfig, error) {
1331+
assert.Contains(t, envs, "CODER_AGENT_NAME=test-container")
1332+
assert.Contains(t, envs, "CODER_WORKSPACE_NAME=test-workspace")
1333+
assert.Contains(t, envs, "CODER_USER_NAME=test-user")
1334+
assert.Contains(t, envs, "CODER_DEPLOYMENT_URL=test-subagent-url")
1335+
return agentcontainers.DevcontainerConfig{}, nil
1336+
})
13251337

13261338
// Make sure the ticker function has been registered
13271339
// before advancing the clock.
@@ -1374,6 +1386,13 @@ func TestAPI(t *testing.T) {
13741386
assert.Empty(t, args)
13751387
return nil
13761388
}) // Exec pwd.
1389+
testutil.RequireSend(ctx, t, fakeDCCLI.readConfigErrC, func(envs []string) (agentcontainers.DevcontainerConfig, error) {
1390+
assert.Contains(t, envs, "CODER_AGENT_NAME=test-container")
1391+
assert.Contains(t, envs, "CODER_WORKSPACE_NAME=test-workspace")
1392+
assert.Contains(t, envs, "CODER_USER_NAME=test-user")
1393+
assert.Contains(t, envs, "CODER_DEPLOYMENT_URL=test-subagent-url")
1394+
return agentcontainers.DevcontainerConfig{}, nil
1395+
})
13771396

13781397
// Wait until the agent recreation is started.
13791398
for len(fakeSAC.createErrC) > 0 {
@@ -1473,6 +1492,72 @@ func TestAPI(t *testing.T) {
14731492
assert.Equal(t, codersdk.DisplayAppVSCodeInsiders, subAgent.DisplayApps[2])
14741493
},
14751494
},
1495+
{
1496+
name: "WithApps",
1497+
customization: &agentcontainers.CoderCustomization{
1498+
Apps: []agentcontainers.SubAgentApp{
1499+
{
1500+
Slug: "web-app",
1501+
DisplayName: ptr.Ref("Web Application"),
1502+
URL: ptr.Ref("http://localhost:8080"),
1503+
OpenIn: codersdk.WorkspaceAppOpenInTab,
1504+
Share: codersdk.WorkspaceAppSharingLevelOwner,
1505+
Icon: ptr.Ref("/icons/web.svg"),
1506+
Order: ptr.Ref(int32(1)),
1507+
},
1508+
{
1509+
Slug: "api-server",
1510+
DisplayName: ptr.Ref("API Server"),
1511+
URL: ptr.Ref("http://localhost:3000"),
1512+
OpenIn: codersdk.WorkspaceAppOpenInSlimWindow,
1513+
Share: codersdk.WorkspaceAppSharingLevelAuthenticated,
1514+
Icon: ptr.Ref("/icons/api.svg"),
1515+
Order: ptr.Ref(int32(2)),
1516+
Hidden: ptr.Ref(true),
1517+
},
1518+
{
1519+
Slug: "docs",
1520+
DisplayName: ptr.Ref("Documentation"),
1521+
URL: ptr.Ref("http://localhost:4000"),
1522+
OpenIn: codersdk.WorkspaceAppOpenInTab,
1523+
Share: codersdk.WorkspaceAppSharingLevelPublic,
1524+
Icon: ptr.Ref("/icons/book.svg"),
1525+
Order: ptr.Ref(int32(3)),
1526+
},
1527+
},
1528+
},
1529+
afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) {
1530+
require.Len(t, subAgent.Apps, 3)
1531+
1532+
// Verify first app
1533+
assert.Equal(t, "web-app", subAgent.Apps[0].Slug)
1534+
assert.Equal(t, "Web Application", *subAgent.Apps[0].DisplayName)
1535+
assert.Equal(t, "http://localhost:8080", *subAgent.Apps[0].URL)
1536+
assert.Equal(t, codersdk.WorkspaceAppOpenInTab, subAgent.Apps[0].OpenIn)
1537+
assert.Equal(t, codersdk.WorkspaceAppSharingLevelOwner, subAgent.Apps[0].Share)
1538+
assert.Equal(t, "/icons/web.svg", *subAgent.Apps[0].Icon)
1539+
assert.Equal(t, int32(1), *subAgent.Apps[0].Order)
1540+
1541+
// Verify second app
1542+
assert.Equal(t, "api-server", subAgent.Apps[1].Slug)
1543+
assert.Equal(t, "API Server", *subAgent.Apps[1].DisplayName)
1544+
assert.Equal(t, "http://localhost:3000", *subAgent.Apps[1].URL)
1545+
assert.Equal(t, codersdk.WorkspaceAppOpenInSlimWindow, subAgent.Apps[1].OpenIn)
1546+
assert.Equal(t, codersdk.WorkspaceAppSharingLevelAuthenticated, subAgent.Apps[1].Share)
1547+
assert.Equal(t, "/icons/api.svg", *subAgent.Apps[1].Icon)
1548+
assert.Equal(t, int32(2), *subAgent.Apps[1].Order)
1549+
assert.Equal(t, true, *subAgent.Apps[1].Hidden)
1550+
1551+
// Verify third app
1552+
assert.Equal(t, "docs", subAgent.Apps[2].Slug)
1553+
assert.Equal(t, "Documentation", *subAgent.Apps[2].DisplayName)
1554+
assert.Equal(t, "http://localhost:4000", *subAgent.Apps[2].URL)
1555+
assert.Equal(t, codersdk.WorkspaceAppOpenInTab, subAgent.Apps[2].OpenIn)
1556+
assert.Equal(t, codersdk.WorkspaceAppSharingLevelPublic, subAgent.Apps[2].Share)
1557+
assert.Equal(t, "/icons/book.svg", *subAgent.Apps[2].Icon)
1558+
assert.Equal(t, int32(3), *subAgent.Apps[2].Order)
1559+
},
1560+
},
14761561
}
14771562

14781563
for _, tt := range tests {

agent/agentcontainers/devcontainercli.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ type DevcontainerCustomizations struct {
3232

3333
type CoderCustomization struct {
3434
DisplayApps []codersdk.DisplayApp `json:"displayApps,omitempty"`
35+
Apps []SubAgentApp `json:"apps,omitempty"`
3536
}
3637

3738
// DevcontainerCLI is an interface for the devcontainer CLI.
3839
type DevcontainerCLI interface {
3940
Up(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIUpOptions) (id string, err error)
4041
Exec(ctx context.Context, workspaceFolder, configPath string, cmd string, cmdArgs []string, opts ...DevcontainerCLIExecOptions) error
41-
ReadConfig(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIReadConfigOptions) (DevcontainerConfig, error)
42+
ReadConfig(ctx context.Context, workspaceFolder, configPath string, env []string, opts ...DevcontainerCLIReadConfigOptions) (DevcontainerConfig, error)
4243
}
4344

4445
// DevcontainerCLIUpOptions are options for the devcontainer CLI Up
@@ -113,8 +114,8 @@ type devcontainerCLIReadConfigConfig struct {
113114
stderr io.Writer
114115
}
115116

116-
// WithExecOutput sets additional stdout and stderr writers for logs
117-
// during Exec operations.
117+
// WithReadConfigOutput sets additional stdout and stderr writers for logs
118+
// during ReadConfig operations.
118119
func WithReadConfigOutput(stdout, stderr io.Writer) DevcontainerCLIReadConfigOptions {
119120
return func(o *devcontainerCLIReadConfigConfig) {
120121
o.stdout = stdout
@@ -256,7 +257,7 @@ func (d *devcontainerCLI) Exec(ctx context.Context, workspaceFolder, configPath
256257
return nil
257258
}
258259

259-
func (d *devcontainerCLI) ReadConfig(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIReadConfigOptions) (DevcontainerConfig, error) {
260+
func (d *devcontainerCLI) ReadConfig(ctx context.Context, workspaceFolder, configPath string, env []string, opts ...DevcontainerCLIReadConfigOptions) (DevcontainerConfig, error) {
260261
conf := applyDevcontainerCLIReadConfigOptions(opts)
261262
logger := d.logger.With(slog.F("workspace_folder", workspaceFolder), slog.F("config_path", configPath))
262263

@@ -269,6 +270,7 @@ func (d *devcontainerCLI) ReadConfig(ctx context.Context, workspaceFolder, confi
269270
}
270271

271272
c := d.execer.CommandContext(ctx, "devcontainer", args...)
273+
c.Env = append(c.Env, env...)
272274

273275
var stdoutBuf bytes.Buffer
274276
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
@@ -308,7 +308,7 @@ func TestDevcontainerCLI_ArgsAndParsing(t *testing.T) {
308308
}
309309

310310
dccli := agentcontainers.NewDevcontainerCLI(logger, testExecer)
311-
config, err := dccli.ReadConfig(ctx, tt.workspaceFolder, tt.configPath, tt.opts...)
311+
config, err := dccli.ReadConfig(ctx, tt.workspaceFolder, tt.configPath, []string{}, tt.opts...)
312312
if tt.wantError {
313313
assert.Error(t, err, "want error")
314314
assert.Equal(t, agentcontainers.DevcontainerConfig{}, config, "expected empty config on error")

0 commit comments

Comments
 (0)