@@ -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,179 @@ 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 : "WithDefaultDisplayApps" ,
1462
+ customization : []agentcontainers.CoderCustomization {},
1463
+ afterCreate : func (t * testing.T , subAgent agentcontainers.SubAgent ) {
1464
+ require .Len (t , subAgent .DisplayApps , 4 )
1465
+ assert .Contains (t , subAgent .DisplayApps , codersdk .DisplayAppVSCodeDesktop )
1466
+ assert .Contains (t , subAgent .DisplayApps , codersdk .DisplayAppWebTerminal )
1467
+ assert .Contains (t , subAgent .DisplayApps , codersdk .DisplayAppSSH )
1468
+ assert .Contains (t , subAgent .DisplayApps , codersdk .DisplayAppPortForward )
1469
+ },
1470
+ },
1471
+ {
1472
+ name : "WithAllDisplayApps" ,
1473
+ customization : []agentcontainers.CoderCustomization {
1474
+ {
1475
+ DisplayApps : map [codersdk.DisplayApp ]bool {
1476
+ codersdk .DisplayAppSSH : true ,
1477
+ codersdk .DisplayAppWebTerminal : true ,
1478
+ codersdk .DisplayAppVSCodeDesktop : true ,
1479
+ codersdk .DisplayAppVSCodeInsiders : true ,
1480
+ codersdk .DisplayAppPortForward : true ,
1481
+ },
1482
+ },
1483
+ },
1484
+ afterCreate : func (t * testing.T , subAgent agentcontainers.SubAgent ) {
1485
+ require .Len (t , subAgent .DisplayApps , 5 )
1486
+ assert .Contains (t , subAgent .DisplayApps , codersdk .DisplayAppSSH )
1487
+ assert .Contains (t , subAgent .DisplayApps , codersdk .DisplayAppWebTerminal )
1488
+ assert .Contains (t , subAgent .DisplayApps , codersdk .DisplayAppVSCodeDesktop )
1489
+ assert .Contains (t , subAgent .DisplayApps , codersdk .DisplayAppVSCodeInsiders )
1490
+ assert .Contains (t , subAgent .DisplayApps , codersdk .DisplayAppPortForward )
1491
+ },
1492
+ },
1493
+ {
1494
+ name : "WithSomeDisplayAppsDisabled" ,
1495
+ customization : []agentcontainers.CoderCustomization {
1496
+ {
1497
+ DisplayApps : map [codersdk.DisplayApp ]bool {
1498
+ codersdk .DisplayAppSSH : false ,
1499
+ codersdk .DisplayAppWebTerminal : false ,
1500
+ codersdk .DisplayAppVSCodeInsiders : false ,
1501
+
1502
+ // We'll enable vscode in this layer, and disable
1503
+ // it in the next layer to ensure a layer can be
1504
+ // disabled.
1505
+ codersdk .DisplayAppVSCodeDesktop : true ,
1506
+
1507
+ // We disable port-forward in this layer, and
1508
+ // then re-enable it in the next layer to ensure
1509
+ // that behavior works.
1510
+ codersdk .DisplayAppPortForward : false ,
1511
+ },
1512
+ },
1513
+ {
1514
+ DisplayApps : map [codersdk.DisplayApp ]bool {
1515
+ codersdk .DisplayAppVSCodeDesktop : false ,
1516
+ codersdk .DisplayAppPortForward : true ,
1517
+ },
1518
+ },
1519
+ },
1520
+ afterCreate : func (t * testing.T , subAgent agentcontainers.SubAgent ) {
1521
+ require .Len (t , subAgent .DisplayApps , 1 )
1522
+ assert .Contains (t , subAgent .DisplayApps , codersdk .DisplayAppPortForward )
1523
+ },
1524
+ },
1525
+ }
1526
+
1527
+ for _ , tt := range tests {
1528
+ t .Run (tt .name , func (t * testing.T ) {
1529
+ t .Parallel ()
1530
+
1531
+ var (
1532
+ ctx = testutil .Context (t , testutil .WaitMedium )
1533
+ logger = testutil .Logger (t )
1534
+ mClock = quartz .NewMock (t )
1535
+ mCCLI = acmock .NewMockContainerCLI (gomock .NewController (t ))
1536
+ fSAC = & fakeSubAgentClient {createErrC : make (chan error , 1 )}
1537
+ fDCCLI = & fakeDevcontainerCLI {
1538
+ readConfig : agentcontainers.DevcontainerConfig {
1539
+ MergedConfiguration : agentcontainers.DevcontainerConfiguration {
1540
+ Customizations : agentcontainers.DevcontainerCustomizations {
1541
+ Coder : tt .customization ,
1542
+ },
1543
+ },
1544
+ },
1545
+ execErrC : make (chan func (cmd string , args ... string ) error , 1 ),
1546
+ }
1547
+
1548
+ testContainer = codersdk.WorkspaceAgentContainer {
1549
+ ID : "test-container-id" ,
1550
+ FriendlyName : "test-container" ,
1551
+ Image : "test-image" ,
1552
+ Running : true ,
1553
+ CreatedAt : time .Now (),
1554
+ Labels : map [string ]string {
1555
+ agentcontainers .DevcontainerLocalFolderLabel : "/workspaces" ,
1556
+ agentcontainers .DevcontainerConfigFileLabel : "/workspace/.devcontainer/devcontainer.json" ,
1557
+ },
1558
+ }
1559
+ )
1560
+
1561
+ coderBin , err := os .Executable ()
1562
+ require .NoError (t , err )
1563
+
1564
+ // Mock the `List` function to always return out test container.
1565
+ mCCLI .EXPECT ().List (gomock .Any ()).Return (codersdk.WorkspaceAgentListContainersResponse {
1566
+ Containers : []codersdk.WorkspaceAgentContainer {testContainer },
1567
+ }, nil ).AnyTimes ()
1568
+
1569
+ // Mock the steps used for injecting the coder agent.
1570
+ gomock .InOrder (
1571
+ mCCLI .EXPECT ().DetectArchitecture (gomock .Any (), testContainer .ID ).Return (runtime .GOARCH , nil ),
1572
+ mCCLI .EXPECT ().ExecAs (gomock .Any (), testContainer .ID , "root" , "mkdir" , "-p" , "/.coder-agent" ).Return (nil , nil ),
1573
+ mCCLI .EXPECT ().Copy (gomock .Any (), testContainer .ID , coderBin , "/.coder-agent/coder" ).Return (nil ),
1574
+ mCCLI .EXPECT ().ExecAs (gomock .Any (), testContainer .ID , "root" , "chmod" , "0755" , "/.coder-agent" , "/.coder-agent/coder" ).Return (nil , nil ),
1575
+ )
1576
+
1577
+ mClock .Set (time .Now ()).MustWait (ctx )
1578
+ tickerTrap := mClock .Trap ().TickerFunc ("updaterLoop" )
1579
+
1580
+ api := agentcontainers .NewAPI (logger ,
1581
+ agentcontainers .WithClock (mClock ),
1582
+ agentcontainers .WithContainerCLI (mCCLI ),
1583
+ agentcontainers .WithDevcontainerCLI (fDCCLI ),
1584
+ agentcontainers .WithSubAgentClient (fSAC ),
1585
+ agentcontainers .WithSubAgentURL ("test-subagent-url" ),
1586
+ agentcontainers .WithWatcher (watcher .NewNoop ()),
1587
+ )
1588
+ defer api .Close ()
1589
+
1590
+ // Close before api.Close() defer to avoid deadlock after test.
1591
+ defer close (fSAC .createErrC )
1592
+ defer close (fDCCLI .execErrC )
1593
+
1594
+ // Given: We allow agent creation and injection to succeed.
1595
+ testutil .RequireSend (ctx , t , fSAC .createErrC , nil )
1596
+ testutil .RequireSend (ctx , t , fDCCLI .execErrC , func (cmd string , args ... string ) error {
1597
+ assert .Equal (t , "pwd" , cmd )
1598
+ assert .Empty (t , args )
1599
+ return nil
1600
+ })
1601
+
1602
+ // Wait until the ticker has been registered.
1603
+ tickerTrap .MustWait (ctx ).MustRelease (ctx )
1604
+ tickerTrap .Close ()
1605
+
1606
+ // Then: We expected it to succeed
1607
+ require .Len (t , fSAC .created , 1 )
1608
+ assert .Equal (t , testContainer .FriendlyName , fSAC .created [0 ].Name )
1609
+
1610
+ if tt .afterCreate != nil {
1611
+ tt .afterCreate (t , fSAC .created [0 ])
1612
+ }
1613
+ })
1614
+ }
1615
+ })
1424
1616
}
1425
1617
1426
1618
// mustFindDevcontainerByPath returns the devcontainer with the given workspace
0 commit comments