@@ -2458,6 +2458,199 @@ func TestAgent_DevcontainersDisabledForSubAgent(t *testing.T) {
2458
2458
require .Contains (t , err .Error (), "Dev Container integration inside other Dev Containers is explicitly not supported." )
2459
2459
}
2460
2460
2461
+ // TestAgent_DevcontainerPrebuildClaim tests that we correctly handle
2462
+ // the claiming process for running devcontainers.
2463
+ //
2464
+ // You can run it manually as follows:
2465
+ //
2466
+ // CODER_TEST_USE_DOCKER=1 go test -count=1 ./agent -run TestAgent_DevcontainerPrebuildClaim
2467
+ func TestAgent_DevcontainerPrebuildClaim (t * testing.T ) {
2468
+ if os .Getenv ("CODER_TEST_USE_DOCKER" ) != "1" {
2469
+ t .Skip ("Set CODER_TEST_USE_DOCKER=1 to run this test" )
2470
+ }
2471
+ if _ , err := exec .LookPath ("devcontainer" ); err != nil {
2472
+ t .Skip ("This test requires the devcontainer CLI: npm install -g @devcontainers/cli" )
2473
+ }
2474
+
2475
+ pool , err := dockertest .NewPool ("" )
2476
+ require .NoError (t , err , "Could not connect to docker" )
2477
+
2478
+ var (
2479
+ ctx = testutil .Context (t , testutil .WaitShort )
2480
+
2481
+ devcontainerID = uuid .New ()
2482
+ devcontainerLogSourceID = uuid .New ()
2483
+
2484
+ workspaceFolder = filepath .Join (t .TempDir (), "project" )
2485
+ devcontainerPath = filepath .Join (workspaceFolder , ".devcontainer" )
2486
+ devcontainerConfig = filepath .Join (devcontainerPath , "devcontainer.json" )
2487
+ )
2488
+
2489
+ // Given: A devcontainer project.
2490
+ t .Logf ("Workspace folder: %s" , workspaceFolder )
2491
+
2492
+ err = os .MkdirAll (devcontainerPath , 0o755 )
2493
+ require .NoError (t , err , "create dev container directory" )
2494
+
2495
+ err = os .WriteFile (devcontainerConfig , []byte (`{
2496
+ "name": "project",
2497
+ "image": "busybox:latest",
2498
+ "cmd": ["sleep", "infinity"],
2499
+ "runArgs": ["--label=` + agentcontainers .DevcontainerIsTestRunLabel + `=true"],
2500
+ "customizations": {
2501
+ "coder": {
2502
+ "apps": [{
2503
+ "slug": "zed",
2504
+ "url": "zed://ssh/${localEnv:CODER_WORKSPACE_AGENT_NAME}.${localEnv:CODER_WORKSPACE_NAME}.${localEnv:CODER_WORKSPACE_OWNER_NAME}.coder${containerWorkspaceFolder}"
2505
+ }]
2506
+ }
2507
+ }
2508
+ }` ), 0o600 )
2509
+ require .NoError (t , err , "write devcontainer config" )
2510
+
2511
+ // Given: A manifest with a devcontainer to be started.
2512
+ manifest := agentsdk.Manifest {
2513
+ OwnerName : "prebuilds" ,
2514
+ WorkspaceName : "prebuilds-xyz-123" ,
2515
+
2516
+ Devcontainers : []codersdk.WorkspaceAgentDevcontainer {
2517
+ {ID : devcontainerID , Name : "test" , WorkspaceFolder : workspaceFolder },
2518
+ },
2519
+ Scripts : []codersdk.WorkspaceAgentScript {
2520
+ {ID : devcontainerID , LogSourceID : devcontainerLogSourceID },
2521
+ },
2522
+ }
2523
+
2524
+ conn , client , _ , _ , _ := setupAgent (t , manifest , 0 , func (_ * agenttest.Client , o * agent.Options ) {
2525
+ o .Devcontainers = true
2526
+ o .DevcontainerAPIOptions = append (o .DevcontainerAPIOptions ,
2527
+ agentcontainers .WithContainerLabelIncludeFilter (agentcontainers .DevcontainerLocalFolderLabel , workspaceFolder ),
2528
+ agentcontainers .WithContainerLabelIncludeFilter (agentcontainers .DevcontainerIsTestRunLabel , "true" ),
2529
+ )
2530
+ })
2531
+
2532
+ testutil .Eventually (ctx , t , func (ctx context.Context ) bool {
2533
+ return slices .Contains (client .GetLifecycleStates (), codersdk .WorkspaceAgentLifecycleReady )
2534
+ }, testutil .IntervalMedium , "agent not ready" )
2535
+
2536
+ var dcPrebuild codersdk.WorkspaceAgentDevcontainer
2537
+ testutil .Eventually (ctx , t , func (ctx context.Context ) bool {
2538
+ resp , err := conn .ListContainers (ctx )
2539
+ require .NoError (t , err )
2540
+
2541
+ for _ , dc := range resp .Devcontainers {
2542
+ if dc .Container == nil {
2543
+ continue
2544
+ }
2545
+
2546
+ v , ok := dc .Container .Labels [agentcontainers .DevcontainerLocalFolderLabel ]
2547
+ if ok && v == workspaceFolder {
2548
+ dcPrebuild = dc
2549
+ return true
2550
+ }
2551
+ }
2552
+
2553
+ return false
2554
+ }, testutil .IntervalMedium , "devcontainer not found" )
2555
+ defer func () {
2556
+ pool .Client .RemoveContainer (docker.RemoveContainerOptions {
2557
+ ID : dcPrebuild .Container .ID ,
2558
+ RemoveVolumes : true ,
2559
+ Force : true ,
2560
+ })
2561
+ }()
2562
+
2563
+ subAgents := client .GetSubAgents ()
2564
+ require .Len (t , subAgents , 1 )
2565
+
2566
+ subAgent := subAgents [0 ]
2567
+ subAgentID , err := uuid .FromBytes (subAgent .GetId ())
2568
+ require .NoError (t , err )
2569
+
2570
+ subAgentApps , err := client .GetSubAgentApps (subAgentID )
2571
+ require .NoError (t , err )
2572
+ require .Len (t , subAgentApps , 1 )
2573
+
2574
+ subAgentApp := subAgentApps [0 ]
2575
+ require .Equal (t , "zed://ssh/project.prebuilds-xyz-123.prebuilds.coder/workspaces/project" , subAgentApp .GetUrl ())
2576
+
2577
+ // Close the client and connection
2578
+ client .Close ()
2579
+ conn .Close ()
2580
+
2581
+ // Given: A manifest with a devcontainer to be started.
2582
+ manifest = agentsdk.Manifest {
2583
+ OwnerName : "user" ,
2584
+ WorkspaceName : "user-workspace" ,
2585
+
2586
+ Devcontainers : []codersdk.WorkspaceAgentDevcontainer {
2587
+ {ID : devcontainerID , Name : "test" , WorkspaceFolder : workspaceFolder },
2588
+ },
2589
+ Scripts : []codersdk.WorkspaceAgentScript {
2590
+ {ID : devcontainerID , LogSourceID : devcontainerLogSourceID },
2591
+ },
2592
+ }
2593
+
2594
+ conn , client , _ , _ , _ = setupAgent (t , manifest , 0 , func (_ * agenttest.Client , o * agent.Options ) {
2595
+ o .Devcontainers = true
2596
+ o .DevcontainerAPIOptions = append (o .DevcontainerAPIOptions ,
2597
+ agentcontainers .WithContainerLabelIncludeFilter (agentcontainers .DevcontainerLocalFolderLabel , workspaceFolder ),
2598
+ agentcontainers .WithContainerLabelIncludeFilter (agentcontainers .DevcontainerIsTestRunLabel , "true" ),
2599
+ )
2600
+ })
2601
+
2602
+ testutil .Eventually (ctx , t , func (ctx context.Context ) bool {
2603
+ return slices .Contains (client .GetLifecycleStates (), codersdk .WorkspaceAgentLifecycleReady )
2604
+ }, testutil .IntervalMedium , "agent not ready" )
2605
+
2606
+ var dcClaimed codersdk.WorkspaceAgentDevcontainer
2607
+ testutil .Eventually (ctx , t , func (ctx context.Context ) bool {
2608
+ resp , err := conn .ListContainers (ctx )
2609
+ require .NoError (t , err )
2610
+
2611
+ for _ , dc := range resp .Devcontainers {
2612
+ if dc .Container == nil {
2613
+ continue
2614
+ }
2615
+
2616
+ v , ok := dc .Container .Labels [agentcontainers .DevcontainerLocalFolderLabel ]
2617
+ if ok && v == workspaceFolder {
2618
+ dcClaimed = dc
2619
+ return true
2620
+ }
2621
+ }
2622
+
2623
+ return false
2624
+ }, testutil .IntervalMedium , "devcontainer not found" )
2625
+ defer func () {
2626
+ if dcClaimed .Container .ID != dcPrebuild .Container .ID {
2627
+ pool .Client .RemoveContainer (docker.RemoveContainerOptions {
2628
+ ID : dcClaimed .Container .ID ,
2629
+ RemoveVolumes : true ,
2630
+ Force : true ,
2631
+ })
2632
+ }
2633
+ }()
2634
+
2635
+ // Then: We expect the claimed devcontainer and prebuild devcontainer
2636
+ // to be using the same underlying container.
2637
+ require .Equal (t , dcPrebuild .Container .ID , dcClaimed .Container .ID )
2638
+
2639
+ subAgents = client .GetSubAgents ()
2640
+ require .Len (t , subAgents , 1 )
2641
+
2642
+ subAgent = subAgents [0 ]
2643
+ subAgentID , err = uuid .FromBytes (subAgent .GetId ())
2644
+ require .NoError (t , err )
2645
+
2646
+ subAgentApps , err = client .GetSubAgentApps (subAgentID )
2647
+ require .NoError (t , err )
2648
+ require .Len (t , subAgentApps , 1 )
2649
+
2650
+ subAgentApp = subAgentApps [0 ]
2651
+ require .Equal (t , "zed://ssh/project.user-workspace.user.coder/workspaces/project" , subAgentApp .GetUrl ())
2652
+ }
2653
+
2461
2654
func TestAgent_Dial (t * testing.T ) {
2462
2655
t .Parallel ()
2463
2656
0 commit comments