Skip to content

Commit dd15026

Browse files
feat(agent/agentcontainers): support displayApps from devcontainer config (#18342)
Updates the agent injection routine to read the dev container's configuration so we can add display apps to the sub agent.
1 parent bc74166 commit dd15026

File tree

11 files changed

+558
-22
lines changed

11 files changed

+558
-22
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.MergedConfiguration.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+
MergedConfiguration: 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)
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