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