Skip to content

Commit 42420b1

Browse files
authored
Merge branch 'main' into cj/prebuild-template-upgrade
2 parents 611db66 + 56ff0fb commit 42420b1

File tree

15 files changed

+657
-44
lines changed

15 files changed

+657
-44
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: 50 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+
ownerName 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,15 @@ func WithSubAgentEnv(env ...string) Option {
153156
}
154157
}
155158

159+
// WithManifestInfo sets the owner name, and workspace name
160+
// for the sub-agent.
161+
func WithManifestInfo(owner, workspace string) Option {
162+
return func(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.
@@ -1051,6 +1063,10 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
10511063
)
10521064
return nil
10531065
}
1066+
if proc.agent.ID == uuid.Nil {
1067+
proc.agent.Architecture = arch
1068+
}
1069+
10541070
agentBinaryPath, err := os.Executable()
10551071
if err != nil {
10561072
return xerrors.Errorf("get agent binary path: %w", err)
@@ -1095,6 +1111,8 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
10951111

10961112
subAgentConfig := proc.agent.CloneConfig(dc)
10971113
if proc.agent.ID == uuid.Nil || maybeRecreateSubAgent {
1114+
subAgentConfig.Architecture = arch
1115+
10981116
// Detect workspace folder by executing `pwd` in the container.
10991117
// NOTE(mafredri): This is a quick and dirty way to detect the
11001118
// workspace folder inside the container. In the future we will
@@ -1127,7 +1145,16 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
11271145
codersdk.DisplayAppPortForward: true,
11281146
}
11291147

1130-
if config, err := api.dccli.ReadConfig(ctx, dc.WorkspaceFolder, dc.ConfigPath); err != nil {
1148+
var appsWithPossibleDuplicates []SubAgentApp
1149+
1150+
if config, err := api.dccli.ReadConfig(ctx, dc.WorkspaceFolder, dc.ConfigPath,
1151+
[]string{
1152+
fmt.Sprintf("CODER_WORKSPACE_AGENT_NAME=%s", dc.Name),
1153+
fmt.Sprintf("CODER_WORKSPACE_OWNER_NAME=%s", api.ownerName),
1154+
fmt.Sprintf("CODER_WORKSPACE_NAME=%s", api.workspaceName),
1155+
fmt.Sprintf("CODER_URL=%s", api.subAgentURL),
1156+
},
1157+
); err != nil {
11311158
api.logger.Error(ctx, "unable to read devcontainer config", slog.Error(err))
11321159
} else {
11331160
coderCustomization := config.MergedConfiguration.Customizations.Coder
@@ -1143,6 +1170,8 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
11431170
}
11441171
displayAppsMap[app] = enabled
11451172
}
1173+
1174+
appsWithPossibleDuplicates = append(appsWithPossibleDuplicates, customization.Apps...)
11461175
}
11471176
}
11481177

@@ -1154,7 +1183,27 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
11541183
}
11551184
slices.Sort(displayApps)
11561185

1186+
appSlugs := make(map[string]struct{})
1187+
apps := make([]SubAgentApp, 0, len(appsWithPossibleDuplicates))
1188+
1189+
// We want to deduplicate the apps based on their slugs here.
1190+
// As we want to prioritize later apps, we will walk through this
1191+
// backwards.
1192+
for _, app := range slices.Backward(appsWithPossibleDuplicates) {
1193+
if _, slugAlreadyExists := appSlugs[app.Slug]; slugAlreadyExists {
1194+
continue
1195+
}
1196+
1197+
appSlugs[app.Slug] = struct{}{}
1198+
apps = append(apps, app)
1199+
}
1200+
1201+
// Apps is currently in reverse order here, so by reversing it we restore
1202+
// it to the original order.
1203+
slices.Reverse(apps)
1204+
11571205
subAgentConfig.DisplayApps = displayApps
1206+
subAgentConfig.Apps = apps
11581207
}
11591208

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

agent/agentcontainers/api_test.go

Lines changed: 141 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ type fakeDevcontainerCLI struct {
6868
execErrC chan func(cmd string, args ...string) error // If set, send fn to return err, nil or close to return execErr.
6969
readConfig agentcontainers.DevcontainerConfig
7070
readConfigErr error
71-
readConfigErrC chan error
71+
readConfigErrC chan func(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
return f.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
if f.readConfigErrC != nil {
104104
select {
105105
case <-ctx.Done():
106106
return agentcontainers.DevcontainerConfig{}, ctx.Err()
107-
case err, ok := <-f.readConfigErrC:
107+
case fn, ok := <-f.readConfigErrC:
108108
if ok {
109-
return f.readConfig, err
109+
return f.readConfig, fn(envs)
110110
}
111111
}
112112
}
@@ -252,6 +252,15 @@ func (m *fakeSubAgentClient) Create(ctx context.Context, agent agentcontainers.S
252252
}
253253
}
254254
}
255+
if agent.Name == "" {
256+
return agentcontainers.SubAgent{}, xerrors.New("name must be set")
257+
}
258+
if agent.Architecture == "" {
259+
return agentcontainers.SubAgent{}, xerrors.New("architecture must be set")
260+
}
261+
if agent.OperatingSystem == "" {
262+
return agentcontainers.SubAgent{}, xerrors.New("operating system must be set")
263+
}
255264
agent.ID = uuid.New()
256265
agent.AuthToken = uuid.New()
257266
if m.agents == nil {
@@ -1253,7 +1262,8 @@ func TestAPI(t *testing.T) {
12531262
deleteErrC: make(chan error, 1),
12541263
}
12551264
fakeDCCLI = &fakeDevcontainerCLI{
1256-
execErrC: make(chan func(cmd string, args ...string) error, 1),
1265+
execErrC: make(chan func(cmd string, args ...string) error, 1),
1266+
readConfigErrC: make(chan func(envs []string) error, 1),
12571267
}
12581268

12591269
testContainer = codersdk.WorkspaceAgentContainer{
@@ -1293,13 +1303,15 @@ func TestAPI(t *testing.T) {
12931303
agentcontainers.WithSubAgentClient(fakeSAC),
12941304
agentcontainers.WithSubAgentURL("test-subagent-url"),
12951305
agentcontainers.WithDevcontainerCLI(fakeDCCLI),
1306+
agentcontainers.WithManifestInfo("test-user", "test-workspace"),
12961307
)
12971308
apiClose := func() {
12981309
closeOnce.Do(func() {
12991310
// Close before api.Close() defer to avoid deadlock after test.
13001311
close(fakeSAC.createErrC)
13011312
close(fakeSAC.deleteErrC)
13021313
close(fakeDCCLI.execErrC)
1314+
close(fakeDCCLI.readConfigErrC)
13031315

13041316
_ = api.Close()
13051317
})
@@ -1313,6 +1325,13 @@ func TestAPI(t *testing.T) {
13131325
assert.Empty(t, args)
13141326
return nil
13151327
}) // Exec pwd.
1328+
testutil.RequireSend(ctx, t, fakeDCCLI.readConfigErrC, func(envs []string) error {
1329+
assert.Contains(t, envs, "CODER_WORKSPACE_AGENT_NAME=test-container")
1330+
assert.Contains(t, envs, "CODER_WORKSPACE_NAME=test-workspace")
1331+
assert.Contains(t, envs, "CODER_WORKSPACE_OWNER_NAME=test-user")
1332+
assert.Contains(t, envs, "CODER_URL=test-subagent-url")
1333+
return nil
1334+
})
13161335

13171336
// Make sure the ticker function has been registered
13181337
// before advancing the clock.
@@ -1453,6 +1472,13 @@ func TestAPI(t *testing.T) {
14531472
assert.Empty(t, args)
14541473
return nil
14551474
}) // Exec pwd.
1475+
testutil.RequireSend(ctx, t, fakeDCCLI.readConfigErrC, func(envs []string) error {
1476+
assert.Contains(t, envs, "CODER_WORKSPACE_AGENT_NAME=test-container")
1477+
assert.Contains(t, envs, "CODER_WORKSPACE_NAME=test-workspace")
1478+
assert.Contains(t, envs, "CODER_WORKSPACE_OWNER_NAME=test-user")
1479+
assert.Contains(t, envs, "CODER_URL=test-subagent-url")
1480+
return nil
1481+
})
14561482

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

16081744
for _, tt := range tests {

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
type CoderCustomization struct {
3435
DisplayApps map[codersdk.DisplayApp]bool `json:"displayApps,omitempty"`
36+
Apps []SubAgentApp `json:"apps,omitempty"`
3537
}
3638

3739
// DevcontainerCLI is an interface for the devcontainer CLI.
3840
type DevcontainerCLI interface {
3941
Up(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIUpOptions) (id string, err error)
4042
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)
43+
ReadConfig(ctx context.Context, workspaceFolder, configPath string, 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-
// during Exec operations.
118+
// WithReadConfigOutput sets additional stdout and stderr writers for logs
119+
// during ReadConfig operations.
118120
func WithReadConfigOutput(stdout, stderr io.Writer) DevcontainerCLIReadConfigOptions {
119121
return func(o *devcontainerCLIReadConfigConfig) {
120122
o.stdout = stdout
@@ -250,7 +252,7 @@ func (d *devcontainerCLI) Exec(ctx context.Context, workspaceFolder, configPath
250252
return nil
251253
}
252254

253-
func (d *devcontainerCLI) ReadConfig(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIReadConfigOptions) (DevcontainerConfig, error) {
255+
func (d *devcontainerCLI) ReadConfig(ctx context.Context, workspaceFolder, configPath string, 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
var stdoutBuf bytes.Buffer
268272
stdoutWriters := []io.Writer{&stdoutBuf, &devcontainerCLILogWriter{ctx: ctx, logger: logger.With(slog.F("stdout", true))}}

0 commit comments

Comments
 (0)