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  }