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