@@ -1937,6 +1937,134 @@ 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+ // 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+
19402068func TestAgent_Dial (t * testing.T ) {
19412069t .Parallel ()
19422070