@@ -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