@@ -60,11 +60,14 @@ func (f *fakeContainerCLI) ExecAs(ctx context.Context, name, user string, args .
60
60
// fakeDevcontainerCLI implements the agentcontainers.DevcontainerCLI
61
61
// interface for testing.
62
62
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
68
71
}
69
72
70
73
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,
95
98
return f .execErr
96
99
}
97
100
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
+
98
115
// fakeWatcher implements the watcher.Watcher interface for testing.
99
116
// It allows controlling what events are sent and when.
100
117
type fakeWatcher struct {
@@ -1132,10 +1149,12 @@ func TestAPI(t *testing.T) {
1132
1149
Containers : []codersdk.WorkspaceAgentContainer {container },
1133
1150
},
1134
1151
}
1152
+ fDCCLI := & fakeDevcontainerCLI {}
1135
1153
1136
1154
logger := slogtest .Make (t ,nil ).Leveled (slog .LevelDebug )
1137
1155
api := agentcontainers .NewAPI (
1138
1156
logger ,
1157
+ agentcontainers .WithDevcontainerCLI (fDCCLI ),
1139
1158
agentcontainers .WithContainerCLI (fLister ),
1140
1159
agentcontainers .WithWatcher (fWatcher ),
1141
1160
agentcontainers .WithClock (mClock ),
@@ -1421,6 +1440,130 @@ func TestAPI(t *testing.T) {
1421
1440
assert .Contains (t ,fakeSAC .deleted ,existingAgentID )
1422
1441
assert .Empty (t ,fakeSAC .agents )
1423
1442
})
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
+ })
1424
1567
}
1425
1568
1426
1569
// mustFindDevcontainerByPath returns the devcontainer with the given workspace