@@ -2458,6 +2458,199 @@ func TestAgent_DevcontainersDisabledForSubAgent(t *testing.T) {
24582458 require .Contains (t , err .Error (), "Dev Container integration inside other Dev Containers is explicitly not supported." )
24592459}
24602460
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+
24612654func TestAgent_Dial (t * testing.T ) {
24622655 t .Parallel ()
24632656
0 commit comments