@@ -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,34 @@ 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 ( )
2226
+ // Ensure the container update routine runs.
2227
+ tickerFuncTrap . MustWait ( ctx ). MustRelease ( ctx )
2228
+ tickerFuncTrap . Close ( )
2229
+ _ , next := mClock . AdvanceNext ( )
2230
+ next . MustWait ( ctx )
2097
2231
2098
- // Use terminal reader so we can see output in case somethin goes wrong.
2099
- tr := testutil .NewTerminalReader (t ,ac )
2232
+ // Verify that a subagent was created.
2233
+ subAgents := agentClient .GetSubAgents ()
2234
+ require .Len (t ,subAgents ,1 ,"expected one sub agent" )
2100
2235
2101
- require .NoError (t ,tr .ReadUntil (ctx ,func (line string )bool {
2102
- return strings .Contains (line ,"#" )|| strings .Contains (line ,"$" )
2103
- }),"find prompt" )
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 )
2104
2240
2105
- wantFileName := "file-from-devcontainer"
2106
- wantFile := filepath .Join (tempWorkspaceFolder ,wantFileName )
2241
+ gotDir ,err := agentClient .GetSubAgentDirectory (subAgentID )
2242
+ require .NoError (t ,err ,"failed to get sub-agent directory" )
2243
+ require .Equal (t ,"/workspaces/mywork" ,gotDir ,"sub-agent directory should match" )
2107
2244
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" )
2245
+ subAgentToken ,err := uuid .FromBytes (subAgent .GetAuthToken ())
2246
+ require .NoError (t ,err ,"failed to parse sub-agent token" )
2112
2247
2113
- // Wait for the connection to close to ensure the touch was executed.
2114
- require .ErrorIs (t ,tr .ReadUntil (ctx ,nil ),io .EOF )
2248
+ payload := testutil .RequireReceive (ctx ,t ,subAgentConnected )
2249
+ require .Equal (t ,subAgentToken .String (),payload .Token ,"sub-agent token should match" )
2250
+ require .Equal (t ,"/workspaces/mywork" ,payload .Directory ,"sub-agent directory should match" )
2115
2251
2116
- _ , err = os . Stat ( wantFile )
2117
- require . NoError ( t , err , "file should exist outside devcontainer" )
2252
+ // Allow the subagent to exit.
2253
+ close ( subAgentReady )
2118
2254
}
2119
2255
2120
2256
// TestAgent_DevcontainerRecreate tests that RecreateDevcontainer
@@ -2173,6 +2309,9 @@ func TestAgent_DevcontainerRecreate(t *testing.T) {
2173
2309
//nolint:dogsled
2174
2310
conn ,client ,_ ,_ ,_ := setupAgent (t ,manifest ,0 ,func (_ * agenttest.Client ,o * agent.Options ) {
2175
2311
o .ExperimentalDevcontainersEnabled = true
2312
+ o .ContainerAPIOptions = append (o .ContainerAPIOptions ,
2313
+ agentcontainers .WithContainerLabelIncludeFilter ("devcontainer.local_folder" ,workspaceFolder ),
2314
+ )
2176
2315
})
2177
2316
2178
2317
ctx := testutil .Context (t ,testutil .WaitLong )