github.com/ddev/ddev@v1.23.2-0.20240519125000-d824ffe36ff3/pkg/ddevapp/ssh_auth.go (about) 1 package ddevapp 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "path" 8 "path/filepath" 9 "text/template" 10 11 "github.com/ddev/ddev/pkg/dockerutil" 12 "github.com/ddev/ddev/pkg/globalconfig" 13 "github.com/ddev/ddev/pkg/util" 14 "github.com/ddev/ddev/pkg/versionconstants" 15 dockerTypes "github.com/docker/docker/api/types" 16 ) 17 18 // SSHAuthName is the "machine name" of the ddev-ssh-agent docker-compose service 19 const SSHAuthName = "ddev-ssh-agent" 20 21 // SSHAuthComposeYAMLPath returns the filepath to the base .ssh-auth-compose yaml file. 22 func SSHAuthComposeYAMLPath() string { 23 globalDir := globalconfig.GetGlobalDdevDir() 24 dest := path.Join(globalDir, ".ssh-auth-compose.yaml") 25 return dest 26 } 27 28 // FullRenderedSSHAuthComposeYAMLPath returns the filepath to the rendered 29 // .ssh-auth-compose-full.yaml file. 30 func FullRenderedSSHAuthComposeYAMLPath() string { 31 globalDir := globalconfig.GetGlobalDdevDir() 32 dest := path.Join(globalDir, ".ssh-auth-compose-full.yaml") 33 return dest 34 } 35 36 // EnsureSSHAgentContainer ensures the ssh-auth container is running. 37 func (app *DdevApp) EnsureSSHAgentContainer() error { 38 sshContainer, err := findDdevSSHAuth() 39 if err != nil { 40 return err 41 } 42 // If we already have a running ssh container, there's nothing to do. 43 if sshContainer != nil && (sshContainer.State == "running" || sshContainer.State == "starting") { 44 return nil 45 } 46 47 dockerutil.EnsureDdevNetwork() 48 49 path, err := app.CreateSSHAuthComposeFile() 50 if err != nil { 51 return err 52 } 53 54 app.DockerEnv() 55 56 // run docker-compose up -d 57 // This will force-recreate, discarding existing auth if there is a stopped container. 58 _, _, err = dockerutil.ComposeCmd(&dockerutil.ComposeCmdOpts{ 59 ComposeFiles: []string{path}, 60 Action: []string{"-p", SSHAuthName, "up", "--build", "--force-recreate", "-d"}, 61 }) 62 if err != nil { 63 return fmt.Errorf("failed to start ddev-ssh-agent: %v", err) 64 } 65 66 // ensure we have a happy sshAuth 67 label := map[string]string{"com.docker.compose.project": SSHAuthName} 68 sshWaitTimeout := 60 69 _, err = dockerutil.ContainerWait(sshWaitTimeout, label) 70 if err != nil { 71 return fmt.Errorf("ddev-ssh-agent failed to become ready; debug with 'docker logs ddev-ssh-agent' and 'docker inspect --format \"{{json .State.Health }}\" ddev-ssh-agent'; error: %v", err) 72 } 73 74 util.Warning("ssh-agent container is running: If you want to add authentication to the ssh-agent container, run 'ddev auth ssh' to enable your keys.") 75 return nil 76 } 77 78 // RemoveSSHAgentContainer brings down the ddev-ssh-agent if it's running. 79 func RemoveSSHAgentContainer() error { 80 // Stop the container if it exists 81 err := dockerutil.RemoveContainer(globalconfig.DdevSSHAgentContainer) 82 if err != nil { 83 if ok := dockerutil.IsErrNotFound(err); !ok { 84 return err 85 } 86 } 87 util.Warning("The ddev-ssh-agent container has been removed. When you start it again you will have to use 'ddev auth ssh' to provide key authentication again.") 88 return nil 89 } 90 91 // CreateSSHAuthComposeFile creates the docker-compose file for the ddev-ssh-agent 92 func (app *DdevApp) CreateSSHAuthComposeFile() (string, error) { 93 94 var doc bytes.Buffer 95 f, ferr := os.Create(SSHAuthComposeYAMLPath()) 96 if ferr != nil { 97 return "", ferr 98 } 99 defer util.CheckClose(f) 100 101 context := "./.sshimageBuild" 102 err := WriteBuildDockerfile(filepath.Join(globalconfig.GetGlobalDdevDir(), context, "Dockerfile"), "", nil, "", "") 103 if err != nil { 104 return "", err 105 } 106 107 uid, gid, username := util.GetContainerUIDGid() 108 109 app.DockerEnv() 110 111 templateVars := map[string]interface{}{ 112 "ssh_auth_image": versionconstants.SSHAuthImage, 113 "ssh_auth_tag": versionconstants.SSHAuthTag, 114 "Username": username, 115 "UID": uid, 116 "GID": gid, 117 "BuildContext": context, 118 } 119 t, err := template.New("ssh_auth_compose_template.yaml").ParseFS(bundledAssets, "ssh_auth_compose_template.yaml") 120 if err != nil { 121 return "", err 122 } 123 err = t.Execute(&doc, templateVars) 124 util.CheckErr(err) 125 _, err = f.WriteString(doc.String()) 126 util.CheckErr(err) 127 128 fullHandle, err := os.Create(FullRenderedSSHAuthComposeYAMLPath()) 129 if err != nil { 130 return "", err 131 } 132 133 userFiles, err := filepath.Glob(filepath.Join(globalconfig.GetGlobalDdevDir(), "ssh-auth-compose.*.yaml")) 134 if err != nil { 135 return "", err 136 } 137 files := append([]string{SSHAuthComposeYAMLPath()}, userFiles...) 138 fullContents, _, err := dockerutil.ComposeCmd(&dockerutil.ComposeCmdOpts{ 139 ComposeFiles: files, 140 Action: []string{"config"}, 141 }) 142 if err != nil { 143 return "", err 144 } 145 _, err = fullHandle.WriteString(fullContents) 146 if err != nil { 147 return "", err 148 } 149 return FullRenderedSSHAuthComposeYAMLPath(), nil 150 } 151 152 // findDdevSSHAuth uses FindContainerByLabels to get our sshAuth container and 153 // return it (or nil if it doesn't exist yet) 154 func findDdevSSHAuth() (*dockerTypes.Container, error) { 155 containerQuery := map[string]string{ 156 "com.docker.compose.project": SSHAuthName, 157 } 158 159 container, err := dockerutil.FindContainerByLabels(containerQuery) 160 if err != nil { 161 return nil, fmt.Errorf("failed to execute findContainersByLabels, %v", err) 162 } 163 return container, nil 164 } 165 166 // RenderSSHAuthStatus returns a user-friendly string showing sshAuth-status 167 func RenderSSHAuthStatus() string { 168 status := GetSSHAuthStatus() 169 var renderedStatus string 170 171 switch status { 172 case "healthy": 173 renderedStatus = util.ColorizeText(status, "green") 174 case "exited": 175 fallthrough 176 default: 177 renderedStatus = util.ColorizeText(status, "red") 178 } 179 return fmt.Sprintf("\nssh-auth status: %v", renderedStatus) 180 } 181 182 // GetSSHAuthStatus outputs sshAuth status and warning if not 183 // running or healthy, as applicable. 184 func GetSSHAuthStatus() string { 185 label := map[string]string{"com.docker.compose.project": SSHAuthName} 186 container, err := dockerutil.FindContainerByLabels(label) 187 188 if err != nil { 189 util.Error("Failed to execute FindContainerByLabels(%v): %v", label, err) 190 return SiteStopped 191 } 192 if container == nil { 193 return SiteStopped 194 } 195 health, _ := dockerutil.GetContainerHealth(container) 196 return health 197 198 }