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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy