@@ -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.
6262type 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
7073func (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.
100117type 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