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  }