@@ -48,6 +48,7 @@ import (
48
48
"cdr.dev/slog/sloggers/slogtest"
49
49
50
50
"github.com/coder/coder/v2/agent"
51
+ "github.com/coder/coder/v2/agent/agentcontainers"
51
52
"github.com/coder/coder/v2/agent/agentssh"
52
53
"github.com/coder/coder/v2/agent/agenttest"
53
54
"github.com/coder/coder/v2/agent/proto"
@@ -60,9 +61,16 @@ import (
60
61
"github.com/coder/coder/v2/tailnet"
61
62
"github.com/coder/coder/v2/tailnet/tailnettest"
62
63
"github.com/coder/coder/v2/testutil"
64
+ "github.com/coder/quartz"
63
65
)
64
66
65
67
func TestMain (m * testing.M ) {
68
+ if os .Getenv ("CODER_TEST_RUN_SUB_AGENT_MAIN" ) == "1" {
69
+ // If we're running as a subagent, we don't want to run the main tests.
70
+ // Instead, we just run the subagent tests.
71
+ exit := runSubAgentMain ()
72
+ os .Exit (exit )
73
+ }
66
74
goleak .VerifyTestMain (m , testutil .GoleakOptions ... )
67
75
}
68
76
@@ -1930,6 +1938,9 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) {
1930
1938
if os .Getenv ("CODER_TEST_USE_DOCKER" ) != "1" {
1931
1939
t .Skip ("Set CODER_TEST_USE_DOCKER=1 to run this test" )
1932
1940
}
1941
+ if _ , err := exec .LookPath ("devcontainer" ); err != nil {
1942
+ t .Skip ("This test requires the devcontainer CLI: npm install -g @devcontainers/cli" )
1943
+ }
1933
1944
1934
1945
pool , err := dockertest .NewPool ("" )
1935
1946
require .NoError (t , err , "Could not connect to docker" )
@@ -1955,6 +1966,9 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) {
1955
1966
// nolint: dogsled
1956
1967
conn , _ , _ , _ , _ := setupAgent (t , agentsdk.Manifest {}, 0 , func (_ * agenttest.Client , o * agent.Options ) {
1957
1968
o .ExperimentalDevcontainersEnabled = true
1969
+ o .ContainerAPIOptions = append (o .ContainerAPIOptions ,
1970
+ agentcontainers .WithContainerLabelIncludeFilter ("this.label.does.not.exist.ignore.devcontainers" , "true" ),
1971
+ )
1958
1972
})
1959
1973
ctx := testutil .Context (t , testutil .WaitLong )
1960
1974
ac , err := conn .ReconnectingPTY (ctx , uuid .New (), 80 , 80 , "/bin/sh" , func (arp * workspacesdk.AgentReconnectingPTYInit ) {
@@ -1986,6 +2000,60 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) {
1986
2000
require .ErrorIs (t , tr .ReadUntil (ctx , nil ), io .EOF )
1987
2001
}
1988
2002
2003
+ type subAgentRequestPayload struct {
2004
+ Token string `json:"token"`
2005
+ Directory string `json:"directory"`
2006
+ }
2007
+
2008
+ // runSubAgentMain is the main function for the sub-agent that connects
2009
+ // to the control plane. It reads the CODER_AGENT_URL and
2010
+ // CODER_AGENT_TOKEN environment variables, sends the token, and exits
2011
+ // with a status code based on the response.
2012
+ func runSubAgentMain () int {
2013
+ url := os .Getenv ("CODER_AGENT_URL" )
2014
+ token := os .Getenv ("CODER_AGENT_TOKEN" )
2015
+ if url == "" || token == "" {
2016
+ _ , _ = fmt .Fprintln (os .Stderr , "CODER_AGENT_URL and CODER_AGENT_TOKEN must be set" )
2017
+ return 10
2018
+ }
2019
+
2020
+ dir , err := os .Getwd ()
2021
+ if err != nil {
2022
+ _ , _ = fmt .Fprintf (os .Stderr , "failed to get current working directory: %v\n " , err )
2023
+ return 1
2024
+ }
2025
+ payload := subAgentRequestPayload {
2026
+ Token : token ,
2027
+ Directory : dir ,
2028
+ }
2029
+ b , err := json .Marshal (payload )
2030
+ if err != nil {
2031
+ _ , _ = fmt .Fprintf (os .Stderr , "failed to marshal payload: %v\n " , err )
2032
+ return 1
2033
+ }
2034
+
2035
+ req , err := http .NewRequest ("POST" , url , bytes .NewReader (b ))
2036
+ if err != nil {
2037
+ _ , _ = fmt .Fprintf (os .Stderr , "failed to create request: %v\n " , err )
2038
+ return 1
2039
+ }
2040
+ ctx , cancel := context .WithTimeout (context .Background (), testutil .WaitLong )
2041
+ defer cancel ()
2042
+ req = req .WithContext (ctx )
2043
+ resp , err := http .DefaultClient .Do (req )
2044
+ if err != nil {
2045
+ _ , _ = fmt .Fprintf (os .Stderr , "agent connection failed: %v\n " , err )
2046
+ return 11
2047
+ }
2048
+ defer resp .Body .Close ()
2049
+ if resp .StatusCode != http .StatusOK {
2050
+ _ , _ = fmt .Fprintf (os .Stderr , "agent exiting with non-zero exit code %d\n " , resp .StatusCode )
2051
+ return 12
2052
+ }
2053
+ _ , _ = fmt .Println ("sub-agent connected successfully" )
2054
+ return 0
2055
+ }
2056
+
1989
2057
// This tests end-to-end functionality of auto-starting a devcontainer.
1990
2058
// It runs "devcontainer up" which creates a real Docker container. As
1991
2059
// such, it does not run by default in CI.
@@ -1999,6 +2067,56 @@ func TestAgent_DevcontainerAutostart(t *testing.T) {
1999
2067
if os .Getenv ("CODER_TEST_USE_DOCKER" ) != "1" {
2000
2068
t .Skip ("Set CODER_TEST_USE_DOCKER=1 to run this test" )
2001
2069
}
2070
+ if _ , err := exec .LookPath ("devcontainer" ); err != nil {
2071
+ t .Skip ("This test requires the devcontainer CLI: npm install -g @devcontainers/cli" )
2072
+ }
2073
+
2074
+ // This HTTP handler handles requests from runSubAgentMain which
2075
+ // acts as a fake sub-agent. We want to verify that the sub-agent
2076
+ // connects and sends its token. We use a channel to signal
2077
+ // that the sub-agent has connected successfully and then we wait
2078
+ // until we receive another signal to return from the handler. This
2079
+ // keeps the agent "alive" for as long as we want.
2080
+ subAgentConnected := make (chan subAgentRequestPayload , 1 )
2081
+ subAgentReady := make (chan struct {}, 1 )
2082
+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
2083
+ t .Logf ("Sub-agent request received: %s %s" , r .Method , r .URL .Path )
2084
+
2085
+ if r .Method != http .MethodPost {
2086
+ http .Error (w , "Method not allowed" , http .StatusMethodNotAllowed )
2087
+ return
2088
+ }
2089
+
2090
+ // Read the token from the request body.
2091
+ var payload subAgentRequestPayload
2092
+ if err := json .NewDecoder (r .Body ).Decode (& payload ); err != nil {
2093
+ http .Error (w , "Failed to read token" , http .StatusBadRequest )
2094
+ t .Logf ("Failed to read token: %v" , err )
2095
+ return
2096
+ }
2097
+ defer r .Body .Close ()
2098
+
2099
+ t .Logf ("Sub-agent request payload received: %+v" , payload )
2100
+
2101
+ // Signal that the sub-agent has connected successfully.
2102
+ select {
2103
+ case <- t .Context ().Done ():
2104
+ t .Logf ("Test context done, not processing sub-agent request" )
2105
+ return
2106
+ case subAgentConnected <- payload :
2107
+ }
2108
+
2109
+ // Wait for the signal to return from the handler.
2110
+ select {
2111
+ case <- t .Context ().Done ():
2112
+ t .Logf ("Test context done, not waiting for sub-agent ready" )
2113
+ return
2114
+ case <- subAgentReady :
2115
+ }
2116
+
2117
+ w .WriteHeader (http .StatusOK )
2118
+ }))
2119
+ defer srv .Close ()
2002
2120
2003
2121
pool , err := dockertest .NewPool ("" )
2004
2122
require .NoError (t , err , "Could not connect to docker" )
@@ -2016,9 +2134,10 @@ func TestAgent_DevcontainerAutostart(t *testing.T) {
2016
2134
require .NoError (t , err , "create devcontainer directory" )
2017
2135
devcontainerFile := filepath .Join (devcontainerPath , "devcontainer.json" )
2018
2136
err = os .WriteFile (devcontainerFile , []byte (`{
2019
- "name": "mywork",
2020
- "image": "busybox:latest",
2021
- "cmd": ["sleep", "infinity"]
2137
+ "name": "mywork",
2138
+ "image": "ubuntu:latest",
2139
+ "cmd": ["sleep", "infinity"],
2140
+ "runArgs": ["--network=host"]
2022
2141
}` ), 0o600 )
2023
2142
require .NoError (t , err , "write devcontainer.json" )
2024
2143
@@ -2043,9 +2162,24 @@ func TestAgent_DevcontainerAutostart(t *testing.T) {
2043
2162
},
2044
2163
},
2045
2164
}
2165
+ mClock := quartz .NewMock (t )
2166
+ mClock .Set (time .Now ())
2167
+ tickerFuncTrap := mClock .Trap ().TickerFunc ("agentcontainers" )
2168
+
2046
2169
//nolint:dogsled
2047
- conn , _ , _ , _ , _ := setupAgent (t , manifest , 0 , func (_ * agenttest.Client , o * agent.Options ) {
2170
+ _ , agentClient , _ , _ , _ := setupAgent (t , manifest , 0 , func (_ * agenttest.Client , o * agent.Options ) {
2048
2171
o .ExperimentalDevcontainersEnabled = true
2172
+ o .ContainerAPIOptions = append (
2173
+ o .ContainerAPIOptions ,
2174
+ // Only match this specific dev container.
2175
+ agentcontainers .WithClock (mClock ),
2176
+ agentcontainers .WithContainerLabelIncludeFilter ("devcontainer.local_folder" , tempWorkspaceFolder ),
2177
+ agentcontainers .WithSubAgentURL (srv .URL ),
2178
+ // The agent will copy "itself", but in the case of this test, the
2179
+ // agent is actually this test binary. So we'll tell the test binary
2180
+ // to execute the sub-agent main function via this env.
2181
+ agentcontainers .WithSubAgentEnv ("CODER_TEST_RUN_SUB_AGENT_MAIN=1" ),
2182
+ )
2049
2183
})
2050
2184
2051
2185
t .Logf ("Waiting for container with label: devcontainer.local_folder=%s" , tempWorkspaceFolder )
@@ -2089,32 +2223,30 @@ func TestAgent_DevcontainerAutostart(t *testing.T) {
2089
2223
2090
2224
ctx := testutil .Context (t , testutil .WaitLong )
2091
2225
2092
- ac , err := conn .ReconnectingPTY (ctx , uuid .New (), 80 , 80 , "" , func (opts * workspacesdk.AgentReconnectingPTYInit ) {
2093
- opts .Container = container .ID
2094
- })
2095
- require .NoError (t , err , "failed to create ReconnectingPTY" )
2096
- defer ac .Close ()
2097
-
2098
- // Use terminal reader so we can see output in case somethin goes wrong.
2099
- tr := testutil .NewTerminalReader (t , ac )
2226
+ // Ensure the container update routine runs.
2227
+ tickerFuncTrap .MustWait (ctx ).MustRelease (ctx )
2228
+ tickerFuncTrap .Close ()
2229
+ _ , next := mClock .AdvanceNext ()
2230
+ next .MustWait (ctx )
2100
2231
2101
- require . NoError ( t , tr . ReadUntil ( ctx , func ( line string ) bool {
2102
- return strings . Contains ( line , "#" ) || strings . Contains ( line , "$" )
2103
- }), "find prompt " )
2232
+ // Verify that a subagent was created.
2233
+ subAgents := agentClient . GetSubAgents ( )
2234
+ require . Len ( t , subAgents , 1 , "expected one sub agent " )
2104
2235
2105
- wantFileName := "file-from-devcontainer"
2106
- wantFile := filepath .Join (tempWorkspaceFolder , wantFileName )
2236
+ subAgent := subAgents [0 ]
2237
+ subAgentID , err := uuid .FromBytes (subAgent .GetId ())
2238
+ require .NoError (t , err , "failed to parse sub-agent ID" )
2239
+ t .Logf ("Connecting to sub-agent: %s (ID: %s)" , subAgent .Name , subAgentID )
2107
2240
2108
- require .NoError (t , json .NewEncoder (ac ).Encode (workspacesdk.ReconnectingPTYRequest {
2109
- // NOTE(mafredri): We must use absolute path here for some reason.
2110
- Data : fmt .Sprintf ("touch /workspaces/mywork/%s; exit\r " , wantFileName ),
2111
- }), "create file inside devcontainer" )
2241
+ subAgentToken , err := uuid .FromBytes (subAgent .GetAuthToken ())
2242
+ require .NoError (t , err , "failed to parse sub-agent token" )
2112
2243
2113
- // Wait for the connection to close to ensure the touch was executed.
2114
- require .ErrorIs (t , tr .ReadUntil (ctx , nil ), io .EOF )
2244
+ payload := testutil .RequireReceive (ctx , t , subAgentConnected )
2245
+ require .Equal (t , subAgentToken .String (), payload .Token , "sub-agent token should match" )
2246
+ require .Equal (t , "/workspaces/mywork" , payload .Directory , "sub-agent directory should match" )
2115
2247
2116
- _ , err = os . Stat ( wantFile )
2117
- require . NoError ( t , err , "file should exist outside devcontainer" )
2248
+ // Allow the subagent to exit.
2249
+ close ( subAgentReady )
2118
2250
}
2119
2251
2120
2252
// TestAgent_DevcontainerRecreate tests that RecreateDevcontainer
@@ -2173,6 +2305,9 @@ func TestAgent_DevcontainerRecreate(t *testing.T) {
2173
2305
//nolint:dogsled
2174
2306
conn , client , _ , _ , _ := setupAgent (t , manifest , 0 , func (_ * agenttest.Client , o * agent.Options ) {
2175
2307
o .ExperimentalDevcontainersEnabled = true
2308
+ o .ContainerAPIOptions = append (o .ContainerAPIOptions ,
2309
+ agentcontainers .WithContainerLabelIncludeFilter ("devcontainer.local_folder" , workspaceFolder ),
2310
+ )
2176
2311
})
2177
2312
2178
2313
ctx := testutil .Context (t , testutil .WaitLong )
0 commit comments