Skip to content

Commit eb8a469

Browse files
feat(agent/agentcontainers): support apps for dev container agents
1 parent 44d4646 commit eb8a469

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: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ type API struct {
6464
subAgentURL string
6565
subAgentEnv []string
6666

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

159+
// WithWorkspaceName sets the workspace name for the sub-agent.
160+
func WithWorkspaceName(name string) Option {
161+
return func(api *API) {
162+
api.workspaceName = name
163+
}
164+
}
165+
166+
// WithUserName sets the workspace name for the sub-agent.
167+
func WithUserName(name string) Option {
168+
return func(api *API) {
169+
api.userName = name
170+
}
171+
}
172+
156173
// WithDevcontainers sets the known devcontainers for the API. This
157174
// allows the API to be aware of devcontainers defined in the workspace
158175
// agent manifest.
@@ -1127,7 +1144,14 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
11271144
codersdk.DisplayAppPortForward: true,
11281145
}
11291146

1130-
if config, err := api.dccli.ReadConfig(ctx, dc.WorkspaceFolder, dc.ConfigPath); err != nil {
1147+
var apps []SubAgentApp
1148+
1149+
if config, err := api.dccli.ReadConfig(ctx, dc.WorkspaceFolder, dc.ConfigPath, []string{
1150+
fmt.Sprintf("CODER_AGENT_NAME=%s", dc.Name),
1151+
fmt.Sprintf("CODER_USER_NAME=%s", api.userName),
1152+
fmt.Sprintf("CODER_WORKSPACE_NAME=%s", api.workspaceName),
1153+
fmt.Sprintf("CODER_DEPLOYMENT_URL=%s", api.subAgentURL),
1154+
}); err != nil {
11311155
api.logger.Error(ctx, "unable to read devcontainer config", slog.Error(err))
11321156
} else {
11331157
coderCustomization := config.MergedConfiguration.Customizations.Coder
@@ -1143,6 +1167,8 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
11431167
}
11441168
displayAppsMap[app] = enabled
11451169
}
1170+
1171+
apps = append(apps, customization.Apps...)
11461172
}
11471173
}
11481174

agent/agentcontainers/api_test.go

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

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

102-
func (f *fakeDevcontainerCLI) ReadConfig(ctx context.Context, _, _ string, _ ...agentcontainers.DevcontainerCLIReadConfigOptions) (agentcontainers.DevcontainerConfig, error) {
103+
func (f *fakeDevcontainerCLI) ReadConfig(ctx context.Context, _, _ string, envs []string, _ ...agentcontainers.DevcontainerCLIReadConfigOptions) (agentcontainers.DevcontainerConfig, error) {
103104
if f.readConfigErrC != nil {
104105
select {
105106
case <-ctx.Done():
106107
return agentcontainers.DevcontainerConfig{}, ctx.Err()
107-
case err, ok := <-f.readConfigErrC:
108+
case fn, ok := <-f.readConfigErrC:
108109
if ok {
109-
return f.readConfig, err
110+
return fn(envs)
110111
}
111112
}
112113
}
@@ -1253,7 +1254,8 @@ func TestAPI(t *testing.T) {
12531254
deleteErrC: make(chan error, 1),
12541255
}
12551256
fakeDCCLI = &fakeDevcontainerCLI{
1256-
execErrC: make(chan func(cmd string, args ...string) error, 1),
1257+
execErrC: make(chan func(cmd string, args ...string) error, 1),
1258+
readConfigErrC: make(chan func(envs []string) (agentcontainers.DevcontainerConfig, error), 1),
12571259
}
12581260

12591261
testContainer = codersdk.WorkspaceAgentContainer{
@@ -1293,13 +1295,16 @@ func TestAPI(t *testing.T) {
12931295
agentcontainers.WithSubAgentClient(fakeSAC),
12941296
agentcontainers.WithSubAgentURL("test-subagent-url"),
12951297
agentcontainers.WithDevcontainerCLI(fakeDCCLI),
1298+
agentcontainers.WithUserName("test-user"),
1299+
agentcontainers.WithWorkspaceName("test-workspace"),
12961300
)
12971301
apiClose := func() {
12981302
closeOnce.Do(func() {
12991303
// Close before api.Close() defer to avoid deadlock after test.
13001304
close(fakeSAC.createErrC)
13011305
close(fakeSAC.deleteErrC)
13021306
close(fakeDCCLI.execErrC)
1307+
defer close(fakeDCCLI.readConfigErrC)
13031308

13041309
_ = api.Close()
13051310
})
@@ -1313,6 +1318,13 @@ func TestAPI(t *testing.T) {
13131318
assert.Empty(t, args)
13141319
return nil
13151320
}) // Exec pwd.
1321+
testutil.RequireSend(ctx, t, fakeDCCLI.readConfigErrC, func(envs []string) (agentcontainers.DevcontainerConfig, error) {
1322+
assert.Contains(t, envs, "CODER_AGENT_NAME=test-container")
1323+
assert.Contains(t, envs, "CODER_WORKSPACE_NAME=test-workspace")
1324+
assert.Contains(t, envs, "CODER_USER_NAME=test-user")
1325+
assert.Contains(t, envs, "CODER_DEPLOYMENT_URL=test-subagent-url")
1326+
return agentcontainers.DevcontainerConfig{}, nil
1327+
})
13161328

13171329
// Make sure the ticker function has been registered
13181330
// before advancing the clock.
@@ -1453,6 +1465,13 @@ func TestAPI(t *testing.T) {
14531465
assert.Empty(t, args)
14541466
return nil
14551467
}) // Exec pwd.
1468+
testutil.RequireSend(ctx, t, fakeDCCLI.readConfigErrC, func(envs []string) (agentcontainers.DevcontainerConfig, error) {
1469+
assert.Contains(t, envs, "CODER_AGENT_NAME=test-container")
1470+
assert.Contains(t, envs, "CODER_WORKSPACE_NAME=test-workspace")
1471+
assert.Contains(t, envs, "CODER_USER_NAME=test-user")
1472+
assert.Contains(t, envs, "CODER_DEPLOYMENT_URL=test-subagent-url")
1473+
return agentcontainers.DevcontainerConfig{}, nil
1474+
})
14561475

14571476
err = api.RefreshContainers(ctx)
14581477
require.NoError(t, err, "refresh containers should not fail")
@@ -1603,6 +1622,74 @@ func TestAPI(t *testing.T) {
16031622
assert.Contains(t, subAgent.DisplayApps, codersdk.DisplayAppPortForward)
16041623
},
16051624
},
1625+
{
1626+
name: "WithApps",
1627+
customization: []agentcontainers.CoderCustomization{
1628+
{
1629+
Apps: []agentcontainers.SubAgentApp{
1630+
{
1631+
Slug: "web-app",
1632+
DisplayName: ptr.Ref("Web Application"),
1633+
URL: ptr.Ref("http://localhost:8080"),
1634+
OpenIn: codersdk.WorkspaceAppOpenInTab,
1635+
Share: codersdk.WorkspaceAppSharingLevelOwner,
1636+
Icon: ptr.Ref("/icons/web.svg"),
1637+
Order: ptr.Ref(int32(1)),
1638+
},
1639+
{
1640+
Slug: "api-server",
1641+
DisplayName: ptr.Ref("API Server"),
1642+
URL: ptr.Ref("http://localhost:3000"),
1643+
OpenIn: codersdk.WorkspaceAppOpenInSlimWindow,
1644+
Share: codersdk.WorkspaceAppSharingLevelAuthenticated,
1645+
Icon: ptr.Ref("/icons/api.svg"),
1646+
Order: ptr.Ref(int32(2)),
1647+
Hidden: ptr.Ref(true),
1648+
},
1649+
{
1650+
Slug: "docs",
1651+
DisplayName: ptr.Ref("Documentation"),
1652+
URL: ptr.Ref("http://localhost:4000"),
1653+
OpenIn: codersdk.WorkspaceAppOpenInTab,
1654+
Share: codersdk.WorkspaceAppSharingLevelPublic,
1655+
Icon: ptr.Ref("/icons/book.svg"),
1656+
Order: ptr.Ref(int32(3)),
1657+
},
1658+
},
1659+
},
1660+
},
1661+
afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) {
1662+
require.Len(t, subAgent.Apps, 3)
1663+
1664+
// Verify first app
1665+
assert.Equal(t, "web-app", subAgent.Apps[0].Slug)
1666+
assert.Equal(t, "Web Application", *subAgent.Apps[0].DisplayName)
1667+
assert.Equal(t, "http://localhost:8080", *subAgent.Apps[0].URL)
1668+
assert.Equal(t, codersdk.WorkspaceAppOpenInTab, subAgent.Apps[0].OpenIn)
1669+
assert.Equal(t, codersdk.WorkspaceAppSharingLevelOwner, subAgent.Apps[0].Share)
1670+
assert.Equal(t, "/icons/web.svg", *subAgent.Apps[0].Icon)
1671+
assert.Equal(t, int32(1), *subAgent.Apps[0].Order)
1672+
1673+
// Verify second app
1674+
assert.Equal(t, "api-server", subAgent.Apps[1].Slug)
1675+
assert.Equal(t, "API Server", *subAgent.Apps[1].DisplayName)
1676+
assert.Equal(t, "http://localhost:3000", *subAgent.Apps[1].URL)
1677+
assert.Equal(t, codersdk.WorkspaceAppOpenInSlimWindow, subAgent.Apps[1].OpenIn)
1678+
assert.Equal(t, codersdk.WorkspaceAppSharingLevelAuthenticated, subAgent.Apps[1].Share)
1679+
assert.Equal(t, "/icons/api.svg", *subAgent.Apps[1].Icon)
1680+
assert.Equal(t, int32(2), *subAgent.Apps[1].Order)
1681+
assert.Equal(t, true, *subAgent.Apps[1].Hidden)
1682+
1683+
// Verify third app
1684+
assert.Equal(t, "docs", subAgent.Apps[2].Slug)
1685+
assert.Equal(t, "Documentation", *subAgent.Apps[2].DisplayName)
1686+
assert.Equal(t, "http://localhost:4000", *subAgent.Apps[2].URL)
1687+
assert.Equal(t, codersdk.WorkspaceAppOpenInTab, subAgent.Apps[2].OpenIn)
1688+
assert.Equal(t, codersdk.WorkspaceAppSharingLevelPublic, subAgent.Apps[2].Share)
1689+
assert.Equal(t, "/icons/book.svg", *subAgent.Apps[2].Icon)
1690+
assert.Equal(t, int32(3), *subAgent.Apps[2].Order)
1691+
},
1692+
},
16061693
}
16071694

16081695
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 map[codersdk.DisplayApp]bool `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
@@ -250,7 +251,7 @@ func (d *devcontainerCLI) Exec(ctx context.Context, workspaceFolder, configPath
250251
return nil
251252
}
252253

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

@@ -263,6 +264,7 @@ func (d *devcontainerCLI) ReadConfig(ctx context.Context, workspaceFolder, confi
263264
}
264265

265266
c := d.execer.CommandContext(ctx, "devcontainer", args...)
267+
c.Env = append(c.Env, env...)
266268

267269
var stdoutBuf bytes.Buffer
268270
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
if tt.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)