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