Skip to content

Commit 3b44a89

Browse files
feat(agent/agentcontainers): support displayApps from devcontainer config
1 parent 70723d3 commit 3b44a89

File tree

11 files changed

+588
-26
lines changed

11 files changed

+588
-26
lines changed

agent/agentcontainers/acmock/acmock.go

Lines changed: 20 additions & 0 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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,13 +1099,25 @@ func (api *API) injectSubAgentIntoContainerLocked(ctx context.Context, dc coders
10991099
directory = DevcontainerDefaultContainerWorkspaceFolder
11001100
}
11011101

1102+
var displayApps []codersdk.DisplayApp
1103+
1104+
if config, err := api.dccli.ReadConfig(ctx, dc.WorkspaceFolder, dc.ConfigPath); err != nil {
1105+
api.logger.Error(ctx, "unable to read devcontainer config", slog.Error(err))
1106+
} else {
1107+
coderCustomization := config.Configuration.Customizations.Coder
1108+
if coderCustomization != nil {
1109+
displayApps = coderCustomization.DisplayApps
1110+
}
1111+
}
1112+
11021113
// The preparation of the subagent is done, now we can create the
11031114
// subagent record in the database to receive the auth token.
11041115
createdAgent, err := api.subAgentClient.Create(ctx, SubAgent{
11051116
Name: dc.Name,
11061117
Directory: directory,
11071118
OperatingSystem: "linux", // Assuming Linux for dev containers.
11081119
Architecture: arch,
1120+
DisplayApps: displayApps,
11091121
})
11101122
if err != nil {
11111123
return xerrors.Errorf("create agent: %w", err)

agent/agentcontainers/api_test.go

Lines changed: 148 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,14 @@ func (f *fakeContainerCLI) ExecAs(ctx context.Context, name, user string, args .
6060
// fakeDevcontainerCLI implements the agentcontainers.DevcontainerCLI
6161
// interface for testing.
6262
type fakeDevcontainerCLI struct {
63-
upID string
64-
upErr error
65-
upErrC chan error // If set, send to return err, close to return upErr.
66-
execErr error
67-
execErrC chan func(cmd string, args ...string) error // If set, send fn to return err, nil or close to return execErr.
63+
upID string
64+
upErr error
65+
upErrC chan error // If set, send to return err, close to return upErr.
66+
execErr error
67+
execErrC chan func(cmd string, args ...string) error // If set, send fn to return err, nil or close to return execErr.
68+
readConfig agentcontainers.DevcontainerConfig
69+
readConfigErr error
70+
readConfigErrC chan error
6871
}
6972

7073
func (f *fakeDevcontainerCLI) Up(ctx context.Context, _, _ string, _ ...agentcontainers.DevcontainerCLIUpOptions) (string, error) {
@@ -95,6 +98,20 @@ func (f *fakeDevcontainerCLI) Exec(ctx context.Context, _, _ string, cmd string,
9598
return f.execErr
9699
}
97100

101+
func (f *fakeDevcontainerCLI) ReadConfig(ctx context.Context, _, _ string, _ ...agentcontainers.DevcontainerCLIReadConfigOptions) (agentcontainers.DevcontainerConfig, error) {
102+
if f.readConfigErrC != nil {
103+
select {
104+
case <-ctx.Done():
105+
return agentcontainers.DevcontainerConfig{}, ctx.Err()
106+
case err, ok := <-f.readConfigErrC:
107+
if ok {
108+
return f.readConfig, err
109+
}
110+
}
111+
}
112+
return f.readConfig, f.readConfigErr
113+
}
114+
98115
// fakeWatcher implements the watcher.Watcher interface for testing.
99116
// It allows controlling what events are sent and when.
100117
type fakeWatcher struct {
@@ -1132,10 +1149,12 @@ func TestAPI(t *testing.T) {
11321149
Containers: []codersdk.WorkspaceAgentContainer{container},
11331150
},
11341151
}
1152+
fDCCLI := &fakeDevcontainerCLI{}
11351153

11361154
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
11371155
api := agentcontainers.NewAPI(
11381156
logger,
1157+
agentcontainers.WithDevcontainerCLI(fDCCLI),
11391158
agentcontainers.WithContainerCLI(fLister),
11401159
agentcontainers.WithWatcher(fWatcher),
11411160
agentcontainers.WithClock(mClock),
@@ -1421,6 +1440,130 @@ func TestAPI(t *testing.T) {
14211440
assert.Contains(t, fakeSAC.deleted, existingAgentID)
14221441
assert.Empty(t, fakeSAC.agents)
14231442
})
1443+
1444+
t.Run("Create", func(t *testing.T) {
1445+
t.Parallel()
1446+
1447+
if runtime.GOOS == "windows" {
1448+
t.Skip("Dev Container tests are not supported on Windows (this test uses mocks but fails due to Windows paths)")
1449+
}
1450+
1451+
tests := []struct {
1452+
name string
1453+
customization *agentcontainers.CoderCustomization
1454+
afterCreate func(t *testing.T, subAgent agentcontainers.SubAgent)
1455+
}{
1456+
{
1457+
name: "WithoutCustomization",
1458+
customization: nil,
1459+
},
1460+
{
1461+
name: "WithDisplayApps",
1462+
customization: &agentcontainers.CoderCustomization{
1463+
DisplayApps: []codersdk.DisplayApp{
1464+
codersdk.DisplayAppSSH,
1465+
codersdk.DisplayAppWebTerminal,
1466+
codersdk.DisplayAppVSCodeInsiders,
1467+
},
1468+
},
1469+
afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) {
1470+
require.Len(t, subAgent.DisplayApps, 3)
1471+
assert.Equal(t, codersdk.DisplayAppSSH, subAgent.DisplayApps[0])
1472+
assert.Equal(t, codersdk.DisplayAppWebTerminal, subAgent.DisplayApps[1])
1473+
assert.Equal(t, codersdk.DisplayAppVSCodeInsiders, subAgent.DisplayApps[2])
1474+
},
1475+
},
1476+
}
1477+
1478+
for _, tt := range tests {
1479+
t.Run(tt.name, func(t *testing.T) {
1480+
t.Parallel()
1481+
1482+
var (
1483+
ctx = testutil.Context(t, testutil.WaitMedium)
1484+
logger = testutil.Logger(t)
1485+
mClock = quartz.NewMock(t)
1486+
mCCLI = acmock.NewMockContainerCLI(gomock.NewController(t))
1487+
fSAC = &fakeSubAgentClient{createErrC: make(chan error, 1)}
1488+
fDCCLI = &fakeDevcontainerCLI{
1489+
readConfig: agentcontainers.DevcontainerConfig{
1490+
Configuration: agentcontainers.DevcontainerConfiguration{
1491+
Customizations: agentcontainers.DevcontainerCustomizations{
1492+
Coder: tt.customization,
1493+
},
1494+
},
1495+
},
1496+
execErrC: make(chan func(cmd string, args ...string) error, 1),
1497+
}
1498+
1499+
testContainer = codersdk.WorkspaceAgentContainer{
1500+
ID: "test-container-id",
1501+
FriendlyName: "test-container",
1502+
Image: "test-image",
1503+
Running: true,
1504+
CreatedAt: time.Now(),
1505+
Labels: map[string]string{
1506+
agentcontainers.DevcontainerLocalFolderLabel: "/workspaces",
1507+
agentcontainers.DevcontainerConfigFileLabel: "/workspace/.devcontainer/devcontainer.json",
1508+
},
1509+
}
1510+
)
1511+
1512+
coderBin, err := os.Executable()
1513+
require.NoError(t, err)
1514+
1515+
// Mock the `List` function to always return out test container.
1516+
mCCLI.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{
1517+
Containers: []codersdk.WorkspaceAgentContainer{testContainer},
1518+
}, nil).AnyTimes()
1519+
1520+
// Mock the steps used for injecting the coder agent.
1521+
gomock.InOrder(
1522+
mCCLI.EXPECT().DetectArchitecture(gomock.Any(), testContainer.ID).Return(runtime.GOARCH, nil),
1523+
mCCLI.EXPECT().ExecAs(gomock.Any(), testContainer.ID, "root", "mkdir", "-p", "/.coder-agent").Return(nil, nil),
1524+
mCCLI.EXPECT().Copy(gomock.Any(), testContainer.ID, coderBin, "/.coder-agent/coder").Return(nil),
1525+
mCCLI.EXPECT().ExecAs(gomock.Any(), testContainer.ID, "root", "chmod", "0755", "/.coder-agent", "/.coder-agent/coder").Return(nil, nil),
1526+
)
1527+
1528+
mClock.Set(time.Now()).MustWait(ctx)
1529+
tickerTrap := mClock.Trap().TickerFunc("updaterLoop")
1530+
1531+
api := agentcontainers.NewAPI(logger,
1532+
agentcontainers.WithClock(mClock),
1533+
agentcontainers.WithContainerCLI(mCCLI),
1534+
agentcontainers.WithDevcontainerCLI(fDCCLI),
1535+
agentcontainers.WithSubAgentClient(fSAC),
1536+
agentcontainers.WithSubAgentURL("test-subagent-url"),
1537+
agentcontainers.WithWatcher(watcher.NewNoop()),
1538+
)
1539+
defer api.Close()
1540+
1541+
// Close before api.Close() defer to avoid deadlock after test.
1542+
defer close(fSAC.createErrC)
1543+
defer close(fDCCLI.execErrC)
1544+
1545+
// Given: We allow agent creation and injection to succeed.
1546+
testutil.RequireSend(ctx, t, fSAC.createErrC, nil)
1547+
testutil.RequireSend(ctx, t, fDCCLI.execErrC, func(cmd string, args ...string) error {
1548+
assert.Equal(t, "pwd", cmd)
1549+
assert.Empty(t, args)
1550+
return nil
1551+
})
1552+
1553+
// Wait until the ticker has been registered.
1554+
tickerTrap.MustWait(ctx).MustRelease(ctx)
1555+
tickerTrap.Close()
1556+
1557+
// Then: We expected it to succeed
1558+
require.Len(t, fSAC.created, 1)
1559+
assert.Equal(t, testContainer.FriendlyName, fSAC.created[0].Name)
1560+
1561+
if tt.afterCreate != nil {
1562+
tt.afterCreate(t, fSAC.created[0])
1563+
}
1564+
})
1565+
}
1566+
})
14241567
}
14251568

14261569
// mustFindDevcontainerByPath returns the devcontainer with the given workspace

0 commit comments

Comments
 (0)