github.com/drud/ddev@v1.21.5-alpha1.0.20230226034409-94fcc4b94453/pkg/ddevapp/router.go (about) 1 package ddevapp 2 3 import ( 4 "bytes" 5 "fmt" 6 "github.com/Masterminds/sprig/v3" 7 "github.com/drud/ddev/pkg/fileutil" 8 "github.com/drud/ddev/pkg/globalconfig" 9 "github.com/drud/ddev/pkg/netutil" 10 "github.com/drud/ddev/pkg/nodeps" 11 "github.com/drud/ddev/pkg/versionconstants" 12 "os" 13 "path" 14 "path/filepath" 15 "sort" 16 "strconv" 17 "strings" 18 "text/template" 19 20 "github.com/drud/ddev/pkg/dockerutil" 21 "github.com/drud/ddev/pkg/util" 22 "github.com/fsouza/go-dockerclient" 23 ) 24 25 // RouterProjectName is the "machine name" of the router docker-compose 26 const RouterProjectName = "ddev-router" 27 28 // RouterComposeYAMLPath returns the full filepath to the routers docker-compose yaml file. 29 func RouterComposeYAMLPath() string { 30 globalDir := globalconfig.GetGlobalDdevDir() 31 dest := path.Join(globalDir, ".router-compose.yaml") 32 return dest 33 } 34 35 // FullRenderedRouterComposeYAMLPath returns the path of the full rendered .router-compose-full.yaml 36 func FullRenderedRouterComposeYAMLPath() string { 37 globalDir := globalconfig.GetGlobalDdevDir() 38 dest := path.Join(globalDir, ".router-compose-full.yaml") 39 return dest 40 } 41 42 // IsRouterDisabled returns true if the router is disabled 43 func IsRouterDisabled(app *DdevApp) bool { 44 if nodeps.IsGitpod() || nodeps.IsCodespaces() { 45 return true 46 } 47 return nodeps.ArrayContainsString(app.GetOmittedContainers(), globalconfig.DdevRouterContainer) 48 } 49 50 // StopRouterIfNoContainers stops the router if there are no ddev containers running. 51 func StopRouterIfNoContainers() error { 52 containersRunning, err := ddevContainersRunning() 53 if err != nil { 54 return err 55 } 56 57 if !containersRunning { 58 err = dockerutil.RemoveContainer(nodeps.RouterContainer, 0) 59 if err != nil { 60 if _, ok := err.(*docker.NoSuchContainer); !ok { 61 return err 62 } 63 } 64 } 65 return nil 66 } 67 68 // StartDdevRouter ensures the router is running. 69 func StartDdevRouter() error { 70 err := os.MkdirAll(filepath.Join(globalconfig.GetGlobalDdevDir(), "router-build"), 0755) 71 if err != nil { 72 return err 73 } 74 // If the router is not healthy/running, we'll kill it so it 75 // starts over again. 76 router, err := FindDdevRouter() 77 if router != nil && err == nil && router.State != "running" { 78 err = dockerutil.RemoveContainer(nodeps.RouterContainer, 0) 79 if err != nil { 80 return err 81 } 82 } 83 84 routerComposeFullPath, err := generateRouterCompose() 85 if err != nil { 86 return err 87 } 88 err = GenerateRouterDockerfile() 89 if err != nil { 90 return err 91 } 92 if globalconfig.DdevGlobalConfig.UseTraefik { 93 err = pushGlobalTraefikConfig() 94 if err != nil { 95 return fmt.Errorf("failed to push global traefik config: %v", err) 96 } 97 } 98 99 err = CheckRouterPorts() 100 if err != nil { 101 return fmt.Errorf("Unable to listen on required ports, %v,\nTroubleshooting suggestions at https://ddev.readthedocs.io/en/stable/users/basics/troubleshooting/#unable-listen", err) 102 } 103 104 // run docker-compose up -d against the ddev-router full compose file 105 _, _, err = dockerutil.ComposeCmd([]string{routerComposeFullPath}, "-p", RouterProjectName, "up", "-d") 106 if err != nil { 107 return fmt.Errorf("failed to start ddev-router: %v", err) 108 } 109 110 // ensure we have a happy router 111 label := map[string]string{"com.docker.compose.service": "ddev-router"} 112 // Normally the router comes right up, but when 113 // it has to do let's encrypt updates, it can take 114 // some time. 115 routerWaitTimeout := 60 116 if globalconfig.DdevGlobalConfig.UseLetsEncrypt { 117 routerWaitTimeout = 180 118 } 119 logOutput, err := dockerutil.ContainerWait(routerWaitTimeout, label) 120 if err != nil { 121 return fmt.Errorf("ddev-router failed to become ready; debug with 'docker logs ddev-router'; logOutput=%s, err=%v", logOutput, err) 122 } 123 124 return nil 125 } 126 127 // generateRouterCompose() generates the ~/.ddev/.router-compose.yaml and ~/.ddev/.router-compose-full.yaml 128 func generateRouterCompose() (string, error) { 129 exposedPorts := determineRouterPorts() 130 131 routerComposeBasePath := RouterComposeYAMLPath() 132 routerComposeFullPath := FullRenderedRouterComposeYAMLPath() 133 134 var doc bytes.Buffer 135 f, ferr := os.Create(routerComposeBasePath) 136 if ferr != nil { 137 return "", ferr 138 } 139 defer util.CheckClose(f) 140 141 dockerIP, _ := dockerutil.GetDockerIP() 142 143 uid, gid, username := util.GetContainerUIDGid() 144 145 templateVars := map[string]interface{}{ 146 "Username": username, 147 "UID": uid, 148 "GID": gid, 149 "router_image": versionconstants.GetRouterImage(), 150 "ports": exposedPorts, 151 "router_bind_all_interfaces": globalconfig.DdevGlobalConfig.RouterBindAllInterfaces, 152 "dockerIP": dockerIP, 153 "disable_http2": globalconfig.DdevGlobalConfig.DisableHTTP2, 154 "letsencrypt": globalconfig.DdevGlobalConfig.UseLetsEncrypt, 155 "letsencrypt_email": globalconfig.DdevGlobalConfig.LetsEncryptEmail, 156 "AutoRestartContainers": globalconfig.DdevGlobalConfig.AutoRestartContainers, 157 "use_traefik": globalconfig.DdevGlobalConfig.UseTraefik, 158 } 159 160 t, err := template.New("router_compose_template.yaml").ParseFS(bundledAssets, "router_compose_template.yaml") 161 if err != nil { 162 return "", err 163 } 164 165 err = t.Execute(&doc, templateVars) 166 if err != nil { 167 return "", err 168 } 169 _, err = f.WriteString(doc.String()) 170 if err != nil { 171 return "", err 172 } 173 174 fullHandle, err := os.Create(routerComposeFullPath) 175 if err != nil { 176 return "", err 177 } 178 179 userFiles, err := filepath.Glob(filepath.Join(globalconfig.GetGlobalDdevDir(), "router-compose.*.yaml")) 180 if err != nil { 181 return "", err 182 } 183 files := append([]string{RouterComposeYAMLPath()}, userFiles...) 184 fullContents, _, err := dockerutil.ComposeCmd(files, "config") 185 if err != nil { 186 return "", err 187 } 188 _, err = fullHandle.WriteString(fullContents) 189 if err != nil { 190 return "", err 191 } 192 193 return routerComposeFullPath, nil 194 } 195 196 // FindDdevRouter uses FindContainerByLabels to get our router container and 197 // return it. 198 func FindDdevRouter() (*docker.APIContainers, error) { 199 containerQuery := map[string]string{ 200 "com.docker.compose.service": RouterProjectName, 201 } 202 container, err := dockerutil.FindContainerByLabels(containerQuery) 203 if err != nil { 204 return nil, fmt.Errorf("failed to execute findContainersByLabels, %v", err) 205 } 206 if container == nil { 207 return nil, fmt.Errorf("No ddev-router was found") 208 } 209 return container, nil 210 } 211 212 // RenderRouterStatus returns a user-friendly string showing router-status 213 func RenderRouterStatus() string { 214 var renderedStatus string 215 if !nodeps.ArrayContainsString(globalconfig.DdevGlobalConfig.OmitContainersGlobal, globalconfig.DdevRouterContainer) { 216 status, logOutput := GetRouterStatus() 217 badRouter := "The router is not healthy. Your projects may not be accessible.\nIf it doesn't become healthy try running 'ddev start' on a project to recreate it." 218 219 switch status { 220 case SiteStopped: 221 renderedStatus = util.ColorizeText(status, "red") + " " + badRouter 222 case "healthy": 223 renderedStatus = util.ColorizeText(status, "green") 224 case "exited": 225 fallthrough 226 default: 227 renderedStatus = util.ColorizeText(status, "red") + " " + badRouter + "\n" + logOutput 228 } 229 } 230 return renderedStatus 231 } 232 233 // GetRouterStatus returns router status and warning if not 234 // running or healthy, as applicable. 235 // return status and most recent log 236 func GetRouterStatus() (string, string) { 237 var status, logOutput string 238 container, err := FindDdevRouter() 239 240 if err != nil || container == nil { 241 status = SiteStopped 242 } else { 243 status, logOutput = dockerutil.GetContainerHealth(container) 244 } 245 246 return status, logOutput 247 } 248 249 // determineRouterPorts returns a list of port mappings retrieved from running site 250 // containers defining VIRTUAL_PORT env var 251 func determineRouterPorts() []string { 252 var routerPorts []string 253 containers, err := dockerutil.FindContainersWithLabel("com.ddev.site-name") 254 if err != nil { 255 util.Failed("failed to retrieve containers for determining port mappings: %v", err) 256 } 257 258 // loop through all containers with site-name label 259 for _, container := range containers { 260 if _, ok := container.Labels["com.ddev.site-name"]; ok { 261 var exposePorts []string 262 263 httpPorts := dockerutil.GetContainerEnv("HTTP_EXPOSE", container) 264 if httpPorts != "" { 265 ports := strings.Split(httpPorts, ",") 266 exposePorts = append(exposePorts, ports...) 267 } 268 269 httpsPorts := dockerutil.GetContainerEnv("HTTPS_EXPOSE", container) 270 if httpsPorts != "" { 271 ports := strings.Split(httpsPorts, ",") 272 exposePorts = append(exposePorts, ports...) 273 } 274 275 for _, exposePortPair := range exposePorts { 276 // ports defined as hostPort:containerPort allow for router to configure upstreams 277 // for containerPort, with server listening on hostPort. exposed ports for router 278 // should be hostPort:hostPort so router can determine what port a request came from 279 // and route the request to the correct upstream 280 exposePort := "" 281 var ports []string 282 283 // Make sure that we are fully numeric in the port pair, and not empty, or ignore 284 _, err = strconv.Atoi(strings.ReplaceAll(exposePortPair, ":", "")) 285 if err != nil { 286 continue 287 } 288 if strings.Contains(exposePortPair, ":") { 289 ports = strings.Split(exposePortPair, ":") 290 } else { 291 // HTTP_EXPOSE and HTTPS_EXPOSE can be a single port, meaning port:port 292 ports = []string{exposePortPair, exposePortPair} 293 } 294 exposePort = ports[0] 295 296 var match bool 297 for _, routerPort := range routerPorts { 298 if exposePort == routerPort { 299 match = true 300 } 301 } 302 303 // if no match, we are adding a new port mapping 304 if !match { 305 routerPorts = append(routerPorts, exposePort) 306 } 307 } 308 } 309 } 310 sort.Slice(routerPorts, func(i, j int) bool { 311 return routerPorts[i] < routerPorts[j] 312 }) 313 314 return routerPorts 315 } 316 317 // CheckRouterPorts tries to connect to the ports the router will use as a heuristic to find out 318 // if they're available for docker to bind to. Returns an error if either one results 319 // in a successful connection. 320 func CheckRouterPorts() error { 321 routerContainer, _ := FindDdevRouter() 322 var existingExposedPorts []string 323 var err error 324 if routerContainer != nil { 325 existingExposedPorts, err = dockerutil.GetExposedContainerPorts(routerContainer.ID) 326 if err != nil { 327 return err 328 } 329 } 330 newRouterPorts := determineRouterPorts() 331 332 for _, port := range newRouterPorts { 333 if nodeps.ArrayContainsString(existingExposedPorts, port) { 334 continue 335 } 336 if netutil.IsPortActive(port) { 337 return fmt.Errorf("port %s is already in use", port) 338 } 339 } 340 return nil 341 } 342 343 func GenerateRouterDockerfile() error { 344 345 type routerData struct { 346 UseTraefik bool 347 } 348 templateData := routerData{ 349 UseTraefik: globalconfig.DdevGlobalConfig.UseTraefik, 350 } 351 352 routerDockerfile := filepath.Join(globalconfig.GetGlobalDdevDir(), "router-build", "Dockerfile") 353 sigExists := true 354 //TODO: Systematize this checking-for-signature, allow an arg to skip if empty 355 fi, err := os.Stat(routerDockerfile) 356 // Don't use simple fileutil.FileExists() because of the danger of an empty file 357 if err == nil && fi.Size() > 0 { 358 // Check to see if file has #ddev-generated in it, meaning we can recreate it. 359 sigExists, err = fileutil.FgrepStringInFile(routerDockerfile, nodeps.DdevFileSignature) 360 if err != nil { 361 return err 362 } 363 } 364 if !sigExists { 365 util.Debug("Not creating %s because it exists and is managed by user", routerDockerfile) 366 } else { 367 f, err := os.Create(routerDockerfile) 368 if err != nil { 369 util.Failed("failed to create router Dockerfile: %v", err) 370 } 371 t, err := template.New("router_Dockerfile_template").Funcs(sprig.TxtFuncMap()).ParseFS(bundledAssets, "router_Dockerfile_template") 372 if err != nil { 373 return fmt.Errorf("could not create template from router_Dockerfile_template: %v", err) 374 } 375 376 err = t.Execute(f, templateData) 377 if err != nil { 378 return fmt.Errorf("could not parse router_Dockerfile_template with templatedate='%v':: %v", templateData, err) 379 } 380 } 381 return nil 382 }