@@ -1937,6 +1937,136 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) {
1937
1937
require .ErrorIs (t , tr .ReadUntil (ctx , nil ), io .EOF )
1938
1938
}
1939
1939
1940
+ // This tests end-to-end functionality of auto-starting a devcontainer.
1941
+ //
1942
+ // connecting to a running container
1943
+ // and executing a command. It creates a real Docker container and runs a
1944
+ // command. As such, it does not run by default in CI.
1945
+ // You can run it manually as follows:
1946
+ //
1947
+ // CODER_TEST_USE_DOCKER=1 go test -count=1 ./agent -run TestAgent_DevcontainerAutostart
1948
+ func TestAgent_DevcontainerAutostart (t * testing.T ) {
1949
+ t .Parallel ()
1950
+ if os .Getenv ("CODER_TEST_USE_DOCKER" ) != "1" {
1951
+ t .Skip ("Set CODER_TEST_USE_DOCKER=1 to run this test" )
1952
+ }
1953
+
1954
+ ctx := testutil .Context (t , testutil .WaitLong )
1955
+
1956
+ // Connect to Docker
1957
+ pool , err := dockertest .NewPool ("" )
1958
+ require .NoError (t , err , "Could not connect to docker" )
1959
+
1960
+ // Prepare temporary devcontainer for test (mywork).
1961
+ devcontainerID := uuid .New ()
1962
+ tempWorkspaceFolder := t .TempDir ()
1963
+ tempWorkspaceFolder = filepath .Join (tempWorkspaceFolder , "mywork" )
1964
+ t .Logf ("Workspace folder: %s" , tempWorkspaceFolder )
1965
+ devcontainerPath := filepath .Join (tempWorkspaceFolder , ".devcontainer" )
1966
+ err = os .MkdirAll (devcontainerPath , 0o755 )
1967
+ require .NoError (t , err , "create devcontainer directory" )
1968
+ devcontainerFile := filepath .Join (devcontainerPath , "devcontainer.json" )
1969
+ err = os .WriteFile (devcontainerFile , []byte (`{
1970
+ "name": "mywork",
1971
+ "image": "busybox:latest",
1972
+ "cmd": ["sleep", "infinity"]
1973
+ }` ), 0o600 )
1974
+ require .NoError (t , err , "write devcontainer.json" )
1975
+
1976
+ manifest := agentsdk.Manifest {
1977
+ // Set up pre-conditions for auto-starting a devcontainer, the script
1978
+ // is expected to be prepared by the provisioner normally.
1979
+ Devcontainers : []codersdk.WorkspaceAgentDevcontainer {
1980
+ {
1981
+ ID : devcontainerID ,
1982
+ Name : "test" ,
1983
+ WorkspaceFolder : tempWorkspaceFolder ,
1984
+ },
1985
+ },
1986
+ Scripts : []codersdk.WorkspaceAgentScript {
1987
+ {
1988
+ ID : devcontainerID ,
1989
+ LogSourceID : agentsdk .ExternalLogSourceID ,
1990
+ RunOnStart : true ,
1991
+ Script : "echo this-will-be-replaced" ,
1992
+ DisplayName : "Dev Container (test)" ,
1993
+ },
1994
+ },
1995
+ }
1996
+ // nolint: dogsled
1997
+ conn , _ , _ , _ , _ := setupAgent (t , manifest , 0 , func (_ * agenttest.Client , o * agent.Options ) {
1998
+ o .ExperimentalDevcontainersEnabled = true
1999
+ })
2000
+
2001
+ t .Logf ("Waiting for container with label: devcontainer.local_folder=%s" , tempWorkspaceFolder )
2002
+
2003
+ var container docker.APIContainers
2004
+ require .Eventually (t , func () bool {
2005
+ containers , err := pool .Client .ListContainers (docker.ListContainersOptions {All : true })
2006
+ if err != nil {
2007
+ t .Logf ("Error listing containers: %v" , err )
2008
+ return false
2009
+ }
2010
+
2011
+ for _ , c := range containers {
2012
+ t .Logf ("Found container: %s with labels: %v" , c .ID [:12 ], c .Labels )
2013
+ if labelValue , ok := c .Labels ["devcontainer.local_folder" ]; ok {
2014
+ if labelValue == tempWorkspaceFolder {
2015
+ t .Logf ("Found matching container: %s" , c .ID [:12 ])
2016
+ container = c
2017
+ return true
2018
+ }
2019
+ }
2020
+ }
2021
+
2022
+ return false
2023
+ }, testutil .WaitSuperLong , testutil .IntervalMedium , "no container with workspace folder label found" )
2024
+
2025
+ t .Cleanup (func () {
2026
+ // We can't rely on pool here because the container is not
2027
+ // managed by it (it is managed by @devcontainer/cli).
2028
+ err := pool .Client .RemoveContainer (docker.RemoveContainerOptions {
2029
+ ID : container .ID ,
2030
+ RemoveVolumes : true ,
2031
+ Force : true ,
2032
+ })
2033
+ assert .NoError (t , err , "remove container" )
2034
+ })
2035
+
2036
+ containerInfo , err := pool .Client .InspectContainer (container .ID )
2037
+ require .NoError (t , err , "inspect container" )
2038
+ t .Logf ("Container state: status: %v" , containerInfo .State .Status )
2039
+ require .True (t , containerInfo .State .Running , "container should be running" )
2040
+
2041
+ ac , err := conn .ReconnectingPTY (ctx , uuid .New (), 80 , 80 , "" , func (opts * workspacesdk.AgentReconnectingPTYInit ) {
2042
+ opts .Container = container .ID
2043
+ })
2044
+ require .NoError (t , err , "failed to create ReconnectingPTY" )
2045
+ defer ac .Close ()
2046
+
2047
+ // Use terminal reader so we can see output in case somethin goes wrong.
2048
+ tr := testutil .NewTerminalReader (t , ac )
2049
+
2050
+ require .NoError (t , tr .ReadUntil (ctx , func (line string ) bool {
2051
+ return strings .Contains (line , "#" ) || strings .Contains (line , "$" )
2052
+ }), "find prompt" )
2053
+
2054
+ wantFileName := "file-from-devcontainer"
2055
+ wantFile := filepath .Join (tempWorkspaceFolder , wantFileName )
2056
+
2057
+ require .NoError (t , json .NewEncoder (ac ).Encode (workspacesdk.ReconnectingPTYRequest {
2058
+ // NOTE(mafredri): We must use absolute path here for some reason.
2059
+ Data : fmt .Sprintf ("touch /workspaces/mywork/%s\r " , wantFileName ),
2060
+ }), "create file inside devcontainer" )
2061
+ require .NoError (t , json .NewEncoder (ac ).Encode (workspacesdk.ReconnectingPTYRequest {Data : "exit\r " }), "write exit command" )
2062
+
2063
+ // Wait for the connection to close.
2064
+ require .ErrorIs (t , tr .ReadUntil (ctx , nil ), io .EOF )
2065
+
2066
+ _ , err = os .Stat (wantFile )
2067
+ require .NoError (t , err , "file should exist outside devcontainer" )
2068
+ }
2069
+
1940
2070
func TestAgent_Dial (t * testing.T ) {
1941
2071
t .Parallel ()
1942
2072
0 commit comments