@@ -1935,8 +1935,6 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) {
1935
1935
t .Skip ("Set CODER_TEST_USE_DOCKER=1 to run this test" )
1936
1936
}
1937
1937
1938
- ctx := testutil .Context (t , testutil .WaitLong )
1939
-
1940
1938
pool , err := dockertest .NewPool ("" )
1941
1939
require .NoError (t , err , "Could not connect to docker" )
1942
1940
ct , err := pool .RunWithOptions (& dockertest.RunOptions {
@@ -1948,10 +1946,10 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) {
1948
1946
config .RestartPolicy = docker.RestartPolicy {Name : "no" }
1949
1947
})
1950
1948
require .NoError (t , err , "Could not start container" )
1951
- t . Cleanup ( func () {
1949
+ defer func () {
1952
1950
err := pool .Purge (ct )
1953
1951
require .NoError (t , err , "Could not stop container" )
1954
- })
1952
+ }( )
1955
1953
// Wait for container to start
1956
1954
require .Eventually (t , func () bool {
1957
1955
ct , ok := pool .ContainerByName (ct .Container .Name )
@@ -1962,6 +1960,7 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) {
1962
1960
conn , _ , _ , _ , _ := setupAgent (t , agentsdk.Manifest {}, 0 , func (_ * agenttest.Client , o * agent.Options ) {
1963
1961
o .ExperimentalDevcontainersEnabled = true
1964
1962
})
1963
+ ctx := testutil .Context (t , testutil .WaitLong )
1965
1964
ac , err := conn .ReconnectingPTY (ctx , uuid .New (), 80 , 80 , "/bin/sh" , func (arp * workspacesdk.AgentReconnectingPTYInit ) {
1966
1965
arp .Container = ct .Container .ID
1967
1966
})
@@ -2005,9 +2004,6 @@ func TestAgent_DevcontainerAutostart(t *testing.T) {
2005
2004
t .Skip ("Set CODER_TEST_USE_DOCKER=1 to run this test" )
2006
2005
}
2007
2006
2008
- ctx := testutil .Context (t , testutil .WaitLong )
2009
-
2010
- // Connect to Docker
2011
2007
pool , err := dockertest .NewPool ("" )
2012
2008
require .NoError (t , err , "Could not connect to docker" )
2013
2009
@@ -2051,7 +2047,7 @@ func TestAgent_DevcontainerAutostart(t *testing.T) {
2051
2047
},
2052
2048
},
2053
2049
}
2054
- // nolint: dogsled
2050
+ //nolint:dogsled
2055
2051
conn , _ , _ , _ , _ := setupAgent (t , manifest , 0 , func (_ * agenttest.Client , o * agent.Options ) {
2056
2052
o .ExperimentalDevcontainersEnabled = true
2057
2053
})
@@ -2079,8 +2075,7 @@ func TestAgent_DevcontainerAutostart(t *testing.T) {
2079
2075
2080
2076
return false
2081
2077
}, testutil .WaitSuperLong , testutil .IntervalMedium , "no container with workspace folder label found" )
2082
-
2083
- t .Cleanup (func () {
2078
+ defer func () {
2084
2079
// We can't rely on pool here because the container is not
2085
2080
// managed by it (it is managed by @devcontainer/cli).
2086
2081
err := pool .Client .RemoveContainer (docker.RemoveContainerOptions {
@@ -2089,13 +2084,15 @@ func TestAgent_DevcontainerAutostart(t *testing.T) {
2089
2084
Force : true ,
2090
2085
})
2091
2086
assert .NoError (t , err , "remove container" )
2092
- })
2087
+ }( )
2093
2088
2094
2089
containerInfo , err := pool .Client .InspectContainer (container .ID )
2095
2090
require .NoError (t , err , "inspect container" )
2096
2091
t .Logf ("Container state: status: %v" , containerInfo .State .Status )
2097
2092
require .True (t , containerInfo .State .Running , "container should be running" )
2098
2093
2094
+ ctx := testutil .Context (t , testutil .WaitLong )
2095
+
2099
2096
ac , err := conn .ReconnectingPTY (ctx , uuid .New (), 80 , 80 , "" , func (opts * workspacesdk.AgentReconnectingPTYInit ) {
2100
2097
opts .Container = container .ID
2101
2098
})
@@ -2124,6 +2121,173 @@ func TestAgent_DevcontainerAutostart(t *testing.T) {
2124
2121
require .NoError (t , err , "file should exist outside devcontainer" )
2125
2122
}
2126
2123
2124
+ // TestAgent_DevcontainerRecreate tests that RecreateDevcontainer
2125
+ // recreates a devcontainer and emits logs.
2126
+ //
2127
+ // This tests end-to-end functionality of auto-starting a devcontainer.
2128
+ // It runs "devcontainer up" which creates a real Docker container. As
2129
+ // such, it does not run by default in CI.
2130
+ //
2131
+ // You can run it manually as follows:
2132
+ //
2133
+ // CODER_TEST_USE_DOCKER=1 go test -count=1 ./agent -run TestAgent_DevcontainerRecreate
2134
+ func TestAgent_DevcontainerRecreate (t * testing.T ) {
2135
+ if os .Getenv ("CODER_TEST_USE_DOCKER" ) != "1" {
2136
+ t .Skip ("Set CODER_TEST_USE_DOCKER=1 to run this test" )
2137
+ }
2138
+ t .Parallel ()
2139
+
2140
+ pool , err := dockertest .NewPool ("" )
2141
+ require .NoError (t , err , "Could not connect to docker" )
2142
+
2143
+ // Prepare temporary devcontainer for test (mywork).
2144
+ devcontainerID := uuid .New ()
2145
+ devcontainerLogSourceID := uuid .New ()
2146
+ workspaceFolder := filepath .Join (t .TempDir (), "mywork" )
2147
+ t .Logf ("Workspace folder: %s" , workspaceFolder )
2148
+ devcontainerPath := filepath .Join (workspaceFolder , ".devcontainer" )
2149
+ err = os .MkdirAll (devcontainerPath , 0o755 )
2150
+ require .NoError (t , err , "create devcontainer directory" )
2151
+ devcontainerFile := filepath .Join (devcontainerPath , "devcontainer.json" )
2152
+ err = os .WriteFile (devcontainerFile , []byte (`{
2153
+ "name": "mywork",
2154
+ "image": "busybox:latest",
2155
+ "cmd": ["sleep", "infinity"]
2156
+ }` ), 0o600 )
2157
+ require .NoError (t , err , "write devcontainer.json" )
2158
+
2159
+ manifest := agentsdk.Manifest {
2160
+ // Set up pre-conditions for auto-starting a devcontainer, the
2161
+ // script is used to extract the log source ID.
2162
+ Devcontainers : []codersdk.WorkspaceAgentDevcontainer {
2163
+ {
2164
+ ID : devcontainerID ,
2165
+ Name : "test" ,
2166
+ WorkspaceFolder : workspaceFolder ,
2167
+ },
2168
+ },
2169
+ Scripts : []codersdk.WorkspaceAgentScript {
2170
+ {
2171
+ ID : devcontainerID ,
2172
+ LogSourceID : devcontainerLogSourceID ,
2173
+ },
2174
+ },
2175
+ }
2176
+
2177
+ //nolint:dogsled
2178
+ conn , client , _ , _ , _ := setupAgent (t , manifest , 0 , func (_ * agenttest.Client , o * agent.Options ) {
2179
+ o .ExperimentalDevcontainersEnabled = true
2180
+ })
2181
+
2182
+ ctx := testutil .Context (t , testutil .WaitLong )
2183
+
2184
+ // We enabled autostart for the devcontainer, so ready is a good
2185
+ // indication that the devcontainer is up and running. Importantly,
2186
+ // this also means that the devcontainer startup is no longer
2187
+ // producing logs that may interfere with the recreate logs.
2188
+ testutil .Eventually (ctx , t , func (context.Context ) bool {
2189
+ states := client .GetLifecycleStates ()
2190
+ return slices .Contains (states , codersdk .WorkspaceAgentLifecycleReady )
2191
+ }, testutil .IntervalMedium , "devcontainer not ready" )
2192
+
2193
+ t .Logf ("Looking for container with label: devcontainer.local_folder=%s" , workspaceFolder )
2194
+
2195
+ var container docker.APIContainers
2196
+ testutil .Eventually (ctx , t , func (context.Context ) bool {
2197
+ containers , err := pool .Client .ListContainers (docker.ListContainersOptions {All : true })
2198
+ if err != nil {
2199
+ t .Logf ("Error listing containers: %v" , err )
2200
+ return false
2201
+ }
2202
+ for _ , c := range containers {
2203
+ t .Logf ("Found container: %s with labels: %v" , c .ID [:12 ], c .Labels )
2204
+ if v , ok := c .Labels ["devcontainer.local_folder" ]; ok && v == workspaceFolder {
2205
+ t .Logf ("Found matching container: %s" , c .ID [:12 ])
2206
+ container = c
2207
+ return true
2208
+ }
2209
+ }
2210
+ return false
2211
+ }, testutil .IntervalMedium , "no container with workspace folder label found" )
2212
+ defer func (container docker.APIContainers ) {
2213
+ // We can't rely on pool here because the container is not
2214
+ // managed by it (it is managed by @devcontainer/cli).
2215
+ err := pool .Client .RemoveContainer (docker.RemoveContainerOptions {
2216
+ ID : container .ID ,
2217
+ RemoveVolumes : true ,
2218
+ Force : true ,
2219
+ })
2220
+ assert .Error (t , err , "container should be removed by recreate" )
2221
+ }(container )
2222
+
2223
+ ctx = testutil .Context (t , testutil .WaitLong ) // Reset context.
2224
+
2225
+ // Capture logs via ScriptLogger.
2226
+ logsCh := make (chan * proto.BatchCreateLogsRequest , 1 )
2227
+ client .SetLogsChannel (logsCh )
2228
+
2229
+ // Invoke recreate to trigger the destruction and recreation of the
2230
+ // devcontainer, we do it in a goroutine so we can process logs
2231
+ // concurrently.
2232
+ go func (container docker.APIContainers ) {
2233
+ err := conn .RecreateDevcontainer (ctx , container .ID )
2234
+ assert .NoError (t , err , "recreate devcontainer should succeed" )
2235
+ }(container )
2236
+
2237
+ t .Logf ("Checking recreate logs for outcome..." )
2238
+
2239
+ // Wait for the logs to be emitted, the @devcontainer/cli up command
2240
+ // will emit a log with the outcome at the end suggesting we did
2241
+ // receive all the logs.
2242
+ waitForOutcomeLoop:
2243
+ for {
2244
+ batch := testutil .RequireReceive (ctx , t , logsCh )
2245
+
2246
+ if bytes .Equal (batch .LogSourceId , devcontainerLogSourceID [:]) {
2247
+ for _ , log := range batch .Logs {
2248
+ t .Logf ("Received log: %s" , log .Output )
2249
+ if strings .Contains (log .Output , "\" outcome\" " ) {
2250
+ break waitForOutcomeLoop
2251
+ }
2252
+ }
2253
+ }
2254
+ }
2255
+
2256
+ t .Logf ("Checking there's a new container with label: devcontainer.local_folder=%s" , workspaceFolder )
2257
+
2258
+ // Make sure the container exists and isn't the same as the old one.
2259
+ testutil .Eventually (ctx , t , func (context.Context ) bool {
2260
+ containers , err := pool .Client .ListContainers (docker.ListContainersOptions {All : true })
2261
+ if err != nil {
2262
+ t .Logf ("Error listing containers: %v" , err )
2263
+ return false
2264
+ }
2265
+ for _ , c := range containers {
2266
+ t .Logf ("Found container: %s with labels: %v" , c .ID [:12 ], c .Labels )
2267
+ if v , ok := c .Labels ["devcontainer.local_folder" ]; ok && v == workspaceFolder {
2268
+ if c .ID == container .ID {
2269
+ t .Logf ("Found same container: %s" , c .ID [:12 ])
2270
+ return false
2271
+ }
2272
+ t .Logf ("Found new container: %s" , c .ID [:12 ])
2273
+ container = c
2274
+ return true
2275
+ }
2276
+ }
2277
+ return false
2278
+ }, testutil .IntervalMedium , "new devcontainer not found" )
2279
+ defer func (container docker.APIContainers ) {
2280
+ // We can't rely on pool here because the container is not
2281
+ // managed by it (it is managed by @devcontainer/cli).
2282
+ err := pool .Client .RemoveContainer (docker.RemoveContainerOptions {
2283
+ ID : container .ID ,
2284
+ RemoveVolumes : true ,
2285
+ Force : true ,
2286
+ })
2287
+ assert .NoError (t , err , "remove container" )
2288
+ }(container )
2289
+ }
2290
+
2127
2291
func TestAgent_Dial (t * testing.T ) {
2128
2292
t .Parallel ()
2129
2293
0 commit comments