github.com/drud/ddev@v1.21.5-alpha1.0.20230226034409-94fcc4b94453/pkg/ddevapp/traefik.go (about) 1 package ddevapp 2 3 import ( 4 "fmt" 5 "github.com/Masterminds/sprig/v3" 6 "github.com/drud/ddev/pkg/dockerutil" 7 "github.com/drud/ddev/pkg/exec" 8 "github.com/drud/ddev/pkg/fileutil" 9 "github.com/drud/ddev/pkg/globalconfig" 10 "github.com/drud/ddev/pkg/nodeps" 11 "github.com/drud/ddev/pkg/util" 12 "os" 13 "path" 14 "path/filepath" 15 "strings" 16 "text/template" 17 ) 18 19 type TraefikRouting struct { 20 ExternalHostnames []string 21 ExternalPort string 22 InternalServiceName string 23 InternalServicePort string 24 HTTPS bool 25 } 26 27 // detectAppRouting reviews the configured services and uses their 28 // VIRTUAL_HOST and HTTP(S)_EXPOSE environment variables to set up routing 29 // for the project 30 func detectAppRouting(app *DdevApp) ([]TraefikRouting, error) { 31 // app.ComposeYaml["services"]; 32 var table []TraefikRouting 33 if services, ok := app.ComposeYaml["services"]; ok { 34 for serviceName, s := range services.(map[string]interface{}) { 35 service := s.(map[string]interface{}) 36 if env, ok := service["environment"].(map[string]interface{}); ok { 37 var virtualHost string 38 var ok bool 39 if virtualHost, ok = env["VIRTUAL_HOST"].(string); ok { 40 util.Debug("VIRTUAL_HOST=%v for %s", virtualHost, serviceName) 41 } 42 if virtualHost == "" { 43 continue 44 } 45 hostnames := strings.Split(virtualHost, ",") 46 if httpExpose, ok := env["HTTP_EXPOSE"].(string); ok { 47 util.Debug("HTTP_EXPOSE=%v for %s", httpExpose, serviceName) 48 routeEntries, err := processHTTPExpose(serviceName, httpExpose, false, hostnames) 49 if err != nil { 50 return nil, err 51 } 52 table = append(table, routeEntries...) 53 } 54 55 if httpsExpose, ok := env["HTTPS_EXPOSE"].(string); ok { 56 util.Debug("HTTPS_EXPOSE=%v for %s", httpsExpose, serviceName) 57 routeEntries, err := processHTTPExpose(serviceName, httpsExpose, true, hostnames) 58 if err != nil { 59 return nil, err 60 } 61 table = append(table, routeEntries...) 62 } 63 } 64 } 65 } 66 return table, nil 67 } 68 69 // processHTTPExpose creates routing table entry from VIRTUAL_HOST and HTTP(S)_EXPOSE 70 // environment variables 71 func processHTTPExpose(serviceName string, httpExpose string, isHTTPS bool, externalHostnames []string) ([]TraefikRouting, error) { 72 var routingTable []TraefikRouting 73 portPairs := strings.Split(httpExpose, ",") 74 for _, portPair := range portPairs { 75 ports := strings.Split(portPair, ":") 76 if len(ports) == 0 || len(ports) > 2 { 77 util.Warning("Skipping bad HTTP_EXPOSE port pair spec %s for service %s", portPair, serviceName) 78 continue 79 } 80 if len(ports) == 1 { 81 ports = append(ports, ports[0]) 82 } 83 routingTable = append(routingTable, TraefikRouting{ExternalHostnames: externalHostnames, ExternalPort: ports[0], InternalServiceName: serviceName, InternalServicePort: ports[1], HTTPS: isHTTPS}) 84 } 85 return routingTable, nil 86 } 87 88 // pushGlobalTraefikConfig pushes the config into ddev-global-cache 89 func pushGlobalTraefikConfig() error { 90 globalTraefikDir := filepath.Join(globalconfig.GetGlobalDdevDir(), "traefik") 91 err := os.MkdirAll(globalTraefikDir, 0755) 92 if err != nil { 93 return fmt.Errorf("failed to create global .ddev/traefik directory: %v", err) 94 } 95 sourceCertsPath := filepath.Join(globalTraefikDir, "certs") 96 // SourceConfigDir for dynamic config 97 sourceConfigDir := filepath.Join(globalTraefikDir, "config") 98 targetCertsPath := path.Join("/mnt/ddev-global-cache/traefik/certs") 99 100 err = os.MkdirAll(sourceCertsPath, 0755) 101 if err != nil { 102 return fmt.Errorf("failed to create global traefik certs dir: %v", err) 103 } 104 err = os.MkdirAll(sourceConfigDir, 0755) 105 if err != nil { 106 return fmt.Errorf("failed to create global traefik config dir: %v", err) 107 } 108 109 // Assume that the #ddev-generated exists in file unless it doesn't 110 sigExists := true 111 for _, pemFile := range []string{"default_cert.crt", "default_key.key"} { 112 origFile := filepath.Join(sourceCertsPath, pemFile) 113 if fileutil.FileExists(origFile) { 114 // Check to see if file has #ddev-generated in it, meaning we can recreate it. 115 sigExists, err = fileutil.FgrepStringInFile(origFile, nodeps.DdevFileSignature) 116 if err != nil { 117 return err 118 } 119 // If either of the files has #ddev-generated, we will respect both 120 if !sigExists { 121 break 122 } 123 } 124 } 125 if sigExists && globalconfig.DdevGlobalConfig.MkcertCARoot != "" { 126 c := []string{"--cert-file", filepath.Join(sourceCertsPath, "default_cert.crt"), "--key-file", filepath.Join(sourceCertsPath, "default_key.key"), "127.0.0.1", "localhost", "*.ddev.local", "ddev-router", "ddev-router.ddev", "ddev-router.ddev_default", "*.ddev.site"} 127 if globalconfig.DdevGlobalConfig.ProjectTldGlobal != "" { 128 c = append(c, "*."+globalconfig.DdevGlobalConfig.ProjectTldGlobal) 129 } 130 131 out, err := exec.RunHostCommand("mkcert", c...) 132 if err != nil { 133 util.Failed("failed to create global mkcert certificate, check mkcert operation: %v", out) 134 } 135 136 // Prepend #ddev-generated in generated crt and key files 137 for _, pemFile := range []string{"default_cert.crt", "default_key.key"} { 138 origFile := filepath.Join(sourceCertsPath, pemFile) 139 140 contents, err := fileutil.ReadFileIntoString(origFile) 141 if err != nil { 142 return fmt.Errorf("failed to read file %v: %v", origFile, err) 143 } 144 contents = nodeps.DdevFileSignature + "\n" + contents 145 err = fileutil.TemplateStringToFile(contents, nil, origFile) 146 if err != nil { 147 return err 148 } 149 } 150 } 151 152 type traefikData struct { 153 App *DdevApp 154 Hostnames []string 155 PrimaryHostname string 156 TargetCertsPath string 157 RouterPorts []string 158 UseLetsEncrypt bool 159 LetsEncryptEmail string 160 } 161 templateData := traefikData{ 162 TargetCertsPath: targetCertsPath, 163 RouterPorts: determineRouterPorts(), 164 UseLetsEncrypt: globalconfig.DdevGlobalConfig.UseLetsEncrypt, 165 LetsEncryptEmail: globalconfig.DdevGlobalConfig.LetsEncryptEmail, 166 } 167 168 traefikYamlFile := filepath.Join(sourceConfigDir, "default_config.yaml") 169 sigExists = true 170 //TODO: Systematize this checking-for-signature, allow an arg to skip if empty 171 fi, err := os.Stat(traefikYamlFile) 172 // Don't use simple fileutil.FileExists() because of the danger of an empty file 173 if err == nil && fi.Size() > 0 { 174 // Check to see if file has #ddev-generated in it, meaning we can recreate it. 175 sigExists, err = fileutil.FgrepStringInFile(traefikYamlFile, nodeps.DdevFileSignature) 176 if err != nil { 177 return err 178 } 179 } 180 if !sigExists { 181 util.Debug("Not creating %s because it exists and is managed by user", traefikYamlFile) 182 } else { 183 f, err := os.Create(traefikYamlFile) 184 if err != nil { 185 util.Failed("failed to create traefik config file: %v", err) 186 } 187 t, err := template.New("traefik_global_config_template.yaml").Funcs(sprig.TxtFuncMap()).ParseFS(bundledAssets, "traefik_global_config_template.yaml") 188 if err != nil { 189 return fmt.Errorf("could not create template from traefik_global_config_template.yaml: %v", err) 190 } 191 192 err = t.Execute(f, templateData) 193 if err != nil { 194 return fmt.Errorf("could not parse traefik_global_config_template.yaml with templatedate='%v':: %v", templateData, err) 195 } 196 } 197 198 // sourceConfigDir for static config 199 sourceConfigDir = globalTraefikDir 200 traefikYamlFile = filepath.Join(sourceConfigDir, "static_config.yaml") 201 sigExists = true 202 //TODO: Systematize this checking-for-signature, allow an arg to skip if empty 203 fi, err = os.Stat(traefikYamlFile) 204 // Don't use simple fileutil.FileExists() because of the danger of an empty file 205 if err == nil && fi.Size() > 0 { 206 // Check to see if file has #ddev-generated in it, meaning we can recreate it. 207 sigExists, err = fileutil.FgrepStringInFile(traefikYamlFile, nodeps.DdevFileSignature) 208 if err != nil { 209 return err 210 } 211 } 212 if !sigExists { 213 util.Debug("Not creating %s because it exists and is managed by user", traefikYamlFile) 214 } else { 215 f, err := os.Create(traefikYamlFile) 216 if err != nil { 217 util.Failed("failed to create traefik config file: %v", err) 218 } 219 t, err := template.New("traefik_static_config_template.yaml").Funcs(sprig.TxtFuncMap()).ParseFS(bundledAssets, "traefik_static_config_template.yaml") 220 if err != nil { 221 return fmt.Errorf("could not create template from traefik_static_config_template.yaml: %v", err) 222 } 223 224 err = t.Execute(f, templateData) 225 if err != nil { 226 return fmt.Errorf("could not parse traefik_global_config_template.yaml with templatedate='%v':: %v", templateData, err) 227 } 228 } 229 uid, _, _ := util.GetContainerUIDGid() 230 231 err = dockerutil.CopyIntoVolume(globalTraefikDir, "ddev-global-cache", "traefik", uid, "", false) 232 if err != nil { 233 return fmt.Errorf("failed to copy global traefik config into docker volume ddev-global-cache/traefik: %v", err) 234 } 235 util.Debug("Copied global traefik config in %s to ddev-global-cache/traefik", sourceCertsPath) 236 237 return nil 238 } 239 240 // configureTraefikForApp configures the dynamic configuration and creates cert+key 241 // in .ddev/traefik 242 func configureTraefikForApp(app *DdevApp) error { 243 routingTable, err := detectAppRouting(app) 244 if err != nil { 245 return err 246 } 247 248 // hostnames here should be used only for creating the cert. 249 hostnames := app.GetHostnames() 250 // There can possibly be VIRTUAL_HOST entries which are not configured hostnames. 251 for _, r := range routingTable { 252 if r.ExternalHostnames != nil { 253 hostnames = append(hostnames, r.ExternalHostnames...) 254 } 255 } 256 hostnames = util.SliceToUniqueSlice(&hostnames) 257 projectTraefikDir := app.GetConfigPath("traefik") 258 err = os.MkdirAll(projectTraefikDir, 0755) 259 if err != nil { 260 return fmt.Errorf("failed to create .ddev/traefik directory: %v", err) 261 } 262 sourceCertsPath := filepath.Join(projectTraefikDir, "certs") 263 sourceConfigDir := filepath.Join(projectTraefikDir, "config") 264 targetCertsPath := path.Join("/mnt/ddev-global-cache/traefik/certs") 265 customCertsPath := app.GetConfigPath("custom_certs") 266 267 err = os.MkdirAll(sourceCertsPath, 0755) 268 if err != nil { 269 return fmt.Errorf("failed to create traefik certs dir: %v", err) 270 } 271 err = os.MkdirAll(sourceConfigDir, 0755) 272 if err != nil { 273 return fmt.Errorf("failed to create traefik config dir: %v", err) 274 } 275 276 baseName := filepath.Join(sourceCertsPath, app.Name) 277 // Assume that the #ddev-generated exists in file unless it doesn't 278 sigExists := true 279 for _, pemFile := range []string{app.Name + ".crt", app.Name + ".key"} { 280 origFile := filepath.Join(sourceCertsPath, pemFile) 281 if fileutil.FileExists(origFile) { 282 // Check to see if file has #ddev-generated in it, meaning we can recreate it. 283 sigExists, err = fileutil.FgrepStringInFile(origFile, nodeps.DdevFileSignature) 284 if err != nil { 285 return err 286 } 287 // If either of the files has #ddev-generated, we will respect both 288 if !sigExists { 289 break 290 } 291 } 292 } 293 // Assuming the certs don't exist, or they have #ddev-generated so can be replaced, create them 294 // But not if we don't have mkcert already set up. 295 if sigExists && globalconfig.DdevGlobalConfig.MkcertCARoot != "" { 296 c := []string{"--cert-file", baseName + ".crt", "--key-file", baseName + ".key", "*.ddev.site", "127.0.0.1", "localhost", "*.ddev.local", "ddev-router", "ddev-router.ddev", "ddev-router.ddev_default"} 297 c = append(c, hostnames...) 298 if app.ProjectTLD != nodeps.DdevDefaultTLD { 299 c = append(c, "*."+app.ProjectTLD) 300 } 301 out, err := exec.RunHostCommand("mkcert", c...) 302 if err != nil { 303 util.Failed("failed to create certificates for project, check mkcert operation: %v; err=%v", out, err) 304 } 305 306 // Prepend #ddev-generated in generated crt and key files 307 for _, pemFile := range []string{app.Name + ".crt", app.Name + ".key"} { 308 origFile := filepath.Join(sourceCertsPath, pemFile) 309 310 contents, err := fileutil.ReadFileIntoString(origFile) 311 if err != nil { 312 return fmt.Errorf("failed to read file %v: %v", origFile, err) 313 } 314 contents = nodeps.DdevFileSignature + "\n" + contents 315 err = fileutil.TemplateStringToFile(contents, nil, origFile) 316 if err != nil { 317 return err 318 } 319 } 320 } 321 322 type traefikData struct { 323 App *DdevApp 324 Hostnames []string 325 PrimaryHostname string 326 TargetCertsPath string 327 RoutingTable []TraefikRouting 328 UseLetsEncrypt bool 329 } 330 templateData := traefikData{ 331 App: app, 332 Hostnames: []string{}, 333 PrimaryHostname: app.GetHostname(), 334 TargetCertsPath: targetCertsPath, 335 RoutingTable: routingTable, 336 UseLetsEncrypt: globalconfig.DdevGlobalConfig.UseLetsEncrypt, 337 } 338 339 // Convert externalHostnames wildcards like `*.<anything>` to `{subdomain:.+}.wild.ddev.site` 340 for i, v := range routingTable { 341 for j, h := range v.ExternalHostnames { 342 if strings.HasPrefix(h, `*.`) { 343 h = `{subdomain:.+}` + strings.TrimPrefix(h, `*`) 344 routingTable[i].ExternalHostnames[j] = h 345 } 346 } 347 } 348 349 traefikYamlFile := filepath.Join(sourceConfigDir, app.Name+".yaml") 350 sigExists = true 351 fi, err := os.Stat(traefikYamlFile) 352 // Don't use simple fileutil.FileExists() because of the danger of an empty file 353 if err == nil && fi.Size() > 0 { 354 // Check to see if file has #ddev-generated in it, meaning we can recreate it. 355 sigExists, err = fileutil.FgrepStringInFile(traefikYamlFile, nodeps.DdevFileSignature) 356 if err != nil { 357 return err 358 } 359 } 360 if !sigExists { 361 util.Debug("Not creating %s because it exists and is managed by user", traefikYamlFile) 362 } else { 363 f, err := os.Create(traefikYamlFile) 364 if err != nil { 365 return fmt.Errorf("failed to create traefik config file: %v", err) 366 } 367 t, err := template.New("traefik_config_template.yaml").Funcs(sprig.TxtFuncMap()).ParseFS(bundledAssets, "traefik_config_template.yaml") 368 if err != nil { 369 return fmt.Errorf("could not create template from traefik_config_template.yaml: %v", err) 370 } 371 372 err = t.Execute(f, templateData) 373 if err != nil { 374 return fmt.Errorf("could not parse traefik_config_template.yaml with templatedate='%v':: %v", templateData, err) 375 } 376 } 377 378 uid, _, _ := util.GetContainerUIDGid() 379 380 err = dockerutil.CopyIntoVolume(projectTraefikDir, "ddev-global-cache", "traefik", uid, "", false) 381 if err != nil { 382 util.Warning("failed to copy traefik into docker volume ddev-global-cache/traefik: %v", err) 383 } else { 384 util.Debug("Copied traefik certs in %s to ddev-global-cache/traefik", sourceCertsPath) 385 } 386 if fileutil.FileExists(filepath.Join(customCertsPath, fmt.Sprintf("%s.crt", app.Name))) { 387 err = dockerutil.CopyIntoVolume(app.GetConfigPath("custom_certs"), "ddev-global-cache", "traefik/certs", uid, "", false) 388 if err != nil { 389 util.Warning("failed copying custom certs into docker volume ddev-global-cache/traefik/certs: %v", err) 390 } else { 391 util.Debug("Copied custom certs in %s to ddev-global-cache/traefik", sourceCertsPath) 392 } 393 } 394 return nil 395 }