Skip to content

Commit 7e69095

Browse files
feat(agent/agentcontainers): support displayApps from devcontainer config
1 parent ae3882a commit 7e69095

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
@@ -1096,13 +1096,25 @@ func (api *API) injectSubAgentIntoContainerLocked(ctx context.Context, dc coders
10961096
directory = DevcontainerDefaultContainerWorkspaceFolder
10971097
}
10981098

1099+
var displayApps []codersdk.DisplayApp
1100+
1101+
if config, err := api.dccli.ReadConfig(ctx, dc.WorkspaceFolder, dc.ConfigPath); err != nil {
1102+
api.logger.Error(ctx, "unable to read devcontainer config", slog.Error(err))
1103+
} else {
1104+
coderCustomization := config.Configuration.Customizations.Coder
1105+
if coderCustomization != nil {
1106+
displayApps = coderCustomization.DisplayApps
1107+
}
1108+
}
1109+
10991110
// The preparation of the subagent is done, now we can create the
11001111
// subagent record in the database to receive the auth token.
11011112
createdAgent, err := api.subAgentClient.Create(ctx, SubAgent{
11021113
Name: dc.Name,
11031114
Directory: directory,
11041115
OperatingSystem: "linux", // Assuming Linux for dev containers.
11051116
Architecture: arch,
1117+
DisplayApps: displayApps,
11061118
})
11071119
if err != nil {
11081120
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),
@@ -1425,6 +1444,130 @@ func TestAPI(t *testing.T) {
14251444
assert.Contains(t, fakeSAC.deleted, existingAgentID)
14261445
assert.Empty(t, fakeSAC.agents)
14271446
})
1447+
1448+
t.Run("Create", func(t *testing.T) {
1449+
t.Parallel()
1450+
1451+
if runtime.GOOS == "windows" {
1452+
t.Skip("Dev Container tests are not supported on Windows (this test uses mocks but fails due to Windows paths)")
1453+
}
1454+
1455+
tests := []struct {
1456+
name string
1457+
customization *agentcontainers.CoderCustomization
1458+
afterCreate func(t *testing.T, subAgent agentcontainers.SubAgent)
1459+
}{
1460+
{
1461+
name: "WithoutCustomization",
1462+
customization: nil,
1463+
},
1464+
{
1465+
name: "WithDisplayApps",
1466+
customization: &agentcontainers.CoderCustomization{
1467+
DisplayApps: []codersdk.DisplayApp{
1468+
codersdk.DisplayAppSSH,
1469+
codersdk.DisplayAppWebTerminal,
1470+
codersdk.DisplayAppVSCodeInsiders,
1471+
},
1472+
},
1473+
afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) {
1474+
require.Len(t, subAgent.DisplayApps, 3)
1475+
assert.Equal(t, codersdk.DisplayAppSSH, subAgent.DisplayApps[0])
1476+
assert.Equal(t, codersdk.DisplayAppWebTerminal, subAgent.DisplayApps[1])
1477+
assert.Equal(t, codersdk.DisplayAppVSCodeInsiders, subAgent.DisplayApps[2])
1478+
},
1479+
},
1480+
}
1481+
1482+
for _, tt := range tests {
1483+
t.Run(tt.name, func(t *testing.T) {
1484+
t.Parallel()
1485+
1486+
var (
1487+
ctx = testutil.Context(t, testutil.WaitMedium)
1488+
logger = testutil.Logger(t)
1489+
mClock = quartz.NewMock(t)
1490+
mCCLI = acmock.NewMockContainerCLI(gomock.NewController(t))
1491+
fSAC = &fakeSubAgentClient{createErrC: make(chan error, 1)}
1492+
fDCCLI = &fakeDevcontainerCLI{
1493+
readConfig: agentcontainers.DevcontainerConfig{
1494+
Configuration: agentcontainers.DevcontainerConfiguration{
1495+
Customizations: agentcontainers.DevcontainerCustomizations{
1496+
Coder: tt.customization,
1497+
},
1498+
},
1499+
},
1500+
execErrC: make(chan func(cmd string, args ...string) error, 1),
1501+
}
1502+
1503+
testContainer = codersdk.WorkspaceAgentContainer{
1504+
ID: "test-container-id",
1505+
FriendlyName: "test-container",
1506+
Image: "test-image",
1507+
Running: true,
1508+
CreatedAt: time.Now(),
1509+
Labels: map[string]string{
1510+
agentcontainers.DevcontainerLocalFolderLabel: "/workspaces",
1511+
agentcontainers.DevcontainerConfigFileLabel: "/workspace/.devcontainer/devcontainer.json",
1512+
},
1513+
}
1514+
)
1515+
1516+
coderBin, err := os.Executable()
1517+
require.NoError(t, err)
1518+
1519+
// Mock the `List` function to always return out test container.
1520+
mCCLI.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{
1521+
Containers: []codersdk.WorkspaceAgentContainer{testContainer},
1522+
}, nil).AnyTimes()
1523+
1524+
// Mock the steps used for injecting the coder agent.
1525+
gomock.InOrder(
1526+
mCCLI.EXPECT().DetectArchitecture(gomock.Any(), testContainer.ID).Return(runtime.GOARCH, nil),
1527+
mCCLI.EXPECT().ExecAs(gomock.Any(), testContainer.ID, "root", "mkdir", "-p", "/.coder-agent").Return(nil, nil),
1528+
mCCLI.EXPECT().Copy(gomock.Any(), testContainer.ID, coderBin, "/.coder-agent/coder").Return(nil),
1529+
mCCLI.EXPECT().ExecAs(gomock.Any(), testContainer.ID, "root", "chmod", "0755", "/.coder-agent", "/.coder-agent/coder").Return(nil, nil),
1530+
)
1531+
1532+
mClock.Set(time.Now()).MustWait(ctx)
1533+
tickerTrap := mClock.Trap().TickerFunc("updaterLoop")
1534+
1535+
api := agentcontainers.NewAPI(logger,
1536+
agentcontainers.WithClock(mClock),
1537+
agentcontainers.WithContainerCLI(mCCLI),
1538+
agentcontainers.WithDevcontainerCLI(fDCCLI),
1539+
agentcontainers.WithSubAgentClient(fSAC),
1540+
agentcontainers.WithSubAgentURL("test-subagent-url"),
1541+
agentcontainers.WithWatcher(watcher.NewNoop()),
1542+
)
1543+
defer api.Close()
1544+
1545+
// Close before api.Close() defer to avoid deadlock after test.
1546+
defer close(fSAC.createErrC)
1547+
defer close(fDCCLI.execErrC)
1548+
1549+
// Given: We allow agent creation and injection to succeed.
1550+
testutil.RequireSend(ctx, t, fSAC.createErrC, nil)
1551+
testutil.RequireSend(ctx, t, fDCCLI.execErrC, func(cmd string, args ...string) error {
1552+
assert.Equal(t, "pwd", cmd)
1553+
assert.Empty(t, args)
1554+
return nil
1555+
})
1556+
1557+
// Wait until the ticker has been registered.
1558+
tickerTrap.MustWait(ctx).MustRelease(ctx)
1559+
tickerTrap.Close()
1560+
1561+
// Then: We expected it to succeed
1562+
require.Len(t, fSAC.created, 1)
1563+
assert.Equal(t, testContainer.FriendlyName, fSAC.created[0].Name)
1564+
1565+
if tt.afterCreate != nil {
1566+
tt.afterCreate(t, fSAC.created[0])
1567+
}
1568+
})
1569+
}
1570+
})
14281571
}
14291572

14301573
// mustFindDevcontainerByPath returns the devcontainer with the given workspace

0 commit comments

Comments
 (0)