Skip to content

Commit 195307e

Browse files
feat(agent/agentcontainers): support apps for dev container agents
1 parent aee96c9 commit 195307e

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)
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