@@ -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,170 @@ 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
+
2139
+ pool ,err := dockertest .NewPool ("" )
2140
+ require .NoError (t ,err ,"Could not connect to docker" )
2141
+
2142
+ // Prepare temporary devcontainer for test (mywork).
2143
+ devcontainerID := uuid .New ()
2144
+ devcontainerLogSourceID := uuid .New ()
2145
+ workspaceFolder := filepath .Join (t .TempDir (),"mywork" )
2146
+ t .Logf ("Workspace folder: %s" ,workspaceFolder )
2147
+ devcontainerPath := filepath .Join (workspaceFolder ,".devcontainer" )
2148
+ err = os .MkdirAll (devcontainerPath ,0o755 )
2149
+ require .NoError (t ,err ,"create devcontainer directory" )
2150
+ devcontainerFile := filepath .Join (devcontainerPath ,"devcontainer.json" )
2151
+ err = os .WriteFile (devcontainerFile , []byte (`{
2152
+ "name": "mywork",
2153
+ "image": "busybox:latest",
2154
+ "cmd": ["sleep", "infinity"]
2155
+ }` ),0o600 )
2156
+ require .NoError (t ,err ,"write devcontainer.json" )
2157
+
2158
+ manifest := agentsdk.Manifest {
2159
+ // Set up pre-conditions for auto-starting a devcontainer, the
2160
+ // script is used to extract the log source ID.
2161
+ Devcontainers : []codersdk.WorkspaceAgentDevcontainer {
2162
+ {
2163
+ ID :devcontainerID ,
2164
+ Name :"test" ,
2165
+ WorkspaceFolder :workspaceFolder ,
2166
+ },
2167
+ },
2168
+ Scripts : []codersdk.WorkspaceAgentScript {
2169
+ {
2170
+ ID :devcontainerID ,
2171
+ LogSourceID :devcontainerLogSourceID ,
2172
+ },
2173
+ },
2174
+ }
2175
+
2176
+ //nolint:dogsled
2177
+ conn ,client ,_ ,_ ,_ := setupAgent (t ,manifest ,0 ,func (_ * agenttest.Client ,o * agent.Options ) {
2178
+ o .ExperimentalDevcontainersEnabled = true
2179
+ })
2180
+
2181
+ // We enabled autostart for the devcontainer, so ready is a good
2182
+ // indication that the devcontainer is up and running. Importantly,
2183
+ // this also means that the devcontainer startup is no longer
2184
+ // producing logs that may interfere with the recreate logs.
2185
+ require .Eventually (t ,func ()bool {
2186
+ states := client .GetLifecycleStates ()
2187
+ return slices .Contains (states ,codersdk .WorkspaceAgentLifecycleReady )
2188
+ },testutil .WaitLong ,testutil .IntervalMedium ,"devcontainer not ready" )
2189
+
2190
+ t .Logf ("Lookging for container with label: devcontainer.local_folder=%s" ,workspaceFolder )
2191
+
2192
+ var container docker.APIContainers
2193
+ require .Eventually (t ,func ()bool {
2194
+ containers ,err := pool .Client .ListContainers (docker.ListContainersOptions {All :true })
2195
+ if err != nil {
2196
+ t .Logf ("Error listing containers: %v" ,err )
2197
+ return false
2198
+ }
2199
+ for _ ,c := range containers {
2200
+ t .Logf ("Found container: %s with labels: %v" ,c .ID [:12 ],c .Labels )
2201
+ if v ,ok := c .Labels ["devcontainer.local_folder" ];ok && v == workspaceFolder {
2202
+ t .Logf ("Found matching container: %s" ,c .ID [:12 ])
2203
+ container = c
2204
+ return true
2205
+ }
2206
+ }
2207
+ return false
2208
+ },testutil .WaitLong ,testutil .IntervalMedium ,"no container with workspace folder label found" )
2209
+ defer func (container docker.APIContainers ) {
2210
+ // We can't rely on pool here because the container is not
2211
+ // managed by it (it is managed by @devcontainer/cli).
2212
+ err := pool .Client .RemoveContainer (docker.RemoveContainerOptions {
2213
+ ID :container .ID ,
2214
+ RemoveVolumes :true ,
2215
+ Force :true ,
2216
+ })
2217
+ assert .Error (t ,err ,"container should be removed by recreate" )
2218
+ }(container )
2219
+
2220
+ ctx := testutil .Context (t ,testutil .WaitLong )
2221
+
2222
+ // Capture logs via ScriptLogger.
2223
+ logsCh := make (chan * proto.BatchCreateLogsRequest ,1 )
2224
+ client .SetLogsChannel (logsCh )
2225
+
2226
+ // Invoke recreate to trigger the destruction and recreation of the
2227
+ // devcontainer, we do it in a goroutine so we can process logs
2228
+ // concurrently.
2229
+ go func (container docker.APIContainers ) {
2230
+ err := conn .RecreateDevcontainer (ctx ,container .ID )
2231
+ assert .NoError (t ,err ,"recreate devcontainer should succeed" )
2232
+ }(container )
2233
+
2234
+ t .Logf ("Checking recreate logs for outcome..." )
2235
+
2236
+ // Wait for the logs to be emitted, the @devcontainer/cli up command
2237
+ // will emit a log with the outcome at the end suggesting we did
2238
+ // receive all the logs.
2239
+ waitForOutcomeLoop:
2240
+ for {
2241
+ batch := testutil .RequireReceive (ctx ,t ,logsCh )
2242
+
2243
+ if bytes .Equal (batch .LogSourceId ,devcontainerLogSourceID [:]) {
2244
+ for _ ,log := range batch .Logs {
2245
+ t .Logf ("Received log: %s" ,log .Output )
2246
+ if strings .Contains (log .Output ,"\" outcome\" " ) {
2247
+ break waitForOutcomeLoop
2248
+ }
2249
+ }
2250
+ }
2251
+ }
2252
+
2253
+ t .Logf ("Checking there's a new container with label: devcontainer.local_folder=%s" ,workspaceFolder )
2254
+
2255
+ // Make sure the container exists and isn't the same as the old one.
2256
+ require .Eventually (t ,func ()bool {
2257
+ containers ,err := pool .Client .ListContainers (docker.ListContainersOptions {All :true })
2258
+ if err != nil {
2259
+ t .Logf ("Error listing containers: %v" ,err )
2260
+ return false
2261
+ }
2262
+ for _ ,c := range containers {
2263
+ t .Logf ("Found container: %s with labels: %v" ,c .ID [:12 ],c .Labels )
2264
+ if v ,ok := c .Labels ["devcontainer.local_folder" ];ok && v == workspaceFolder {
2265
+ if c .ID == container .ID {
2266
+ t .Logf ("Found same container: %s" ,c .ID [:12 ])
2267
+ return false
2268
+ }
2269
+ t .Logf ("Found new container: %s" ,c .ID [:12 ])
2270
+ container = c
2271
+ return true
2272
+ }
2273
+ }
2274
+ return false
2275
+ },testutil .WaitLong ,testutil .IntervalMedium ,"new devcontainer not found" )
2276
+ defer func (container docker.APIContainers ) {
2277
+ // We can't rely on pool here because the container is not
2278
+ // managed by it (it is managed by @devcontainer/cli).
2279
+ err := pool .Client .RemoveContainer (docker.RemoveContainerOptions {
2280
+ ID :container .ID ,
2281
+ RemoveVolumes :true ,
2282
+ Force :true ,
2283
+ })
2284
+ assert .NoError (t ,err ,"remove container" )
2285
+ }(container )
2286
+ }
2287
+
2127
2288
func TestAgent_Dial (t * testing.T ) {
2128
2289
t .Parallel ()
2129
2290