@@ -1937,6 +1937,136 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) {
19371937require .ErrorIs (t ,tr .ReadUntil (ctx ,nil ),io .EOF )
19381938}
19391939
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+
19402070func TestAgent_Dial (t * testing.T ) {
19412071t .Parallel ()
19422072