@@ -1937,6 +1937,135 @@ 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; exit\r " ,wantFileName ),
2060
+ }),"create file inside devcontainer" )
2061
+
2062
+ // Wait for the connection to close to ensure the touch was executed.
2063
+ require .ErrorIs (t ,tr .ReadUntil (ctx ,nil ),io .EOF )
2064
+
2065
+ _ ,err = os .Stat (wantFile )
2066
+ require .NoError (t ,err ,"file should exist outside devcontainer" )
2067
+ }
2068
+
1940
2069
func TestAgent_Dial (t * testing.T ) {
1941
2070
t .Parallel ()
1942
2071