github.com/drud/ddev@v1.21.5-alpha1.0.20230226034409-94fcc4b94453/pkg/dockerutil/dockerutils.go (about)

     1  package dockerutil
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"net"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"regexp"
    14  	"runtime"
    15  	"sort"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  
    20  	ddevexec "github.com/drud/ddev/pkg/exec"
    21  	"github.com/drud/ddev/pkg/fileutil"
    22  	"github.com/drud/ddev/pkg/globalconfig"
    23  	"github.com/drud/ddev/pkg/versionconstants"
    24  
    25  	"net/url"
    26  
    27  	"github.com/drud/ddev/pkg/archive"
    28  	"github.com/drud/ddev/pkg/nodeps"
    29  	"github.com/drud/ddev/pkg/util"
    30  
    31  	"github.com/Masterminds/semver/v3"
    32  	"github.com/drud/ddev/pkg/output"
    33  	docker "github.com/fsouza/go-dockerclient"
    34  )
    35  
    36  // NetName provides the default network name for ddev.
    37  const NetName = "ddev_default"
    38  
    39  // EnsureNetwork will ensure the docker network for ddev is created.
    40  func EnsureNetwork(client *docker.Client, name string) error {
    41  	if !NetExists(client, name) {
    42  		netOptions := docker.CreateNetworkOptions{
    43  			Name:     name,
    44  			Driver:   "bridge",
    45  			Internal: false,
    46  		}
    47  		_, err := client.CreateNetwork(netOptions)
    48  		if err != nil {
    49  			return err
    50  		}
    51  		output.UserOut.Println("Network", name, "created")
    52  
    53  	}
    54  	return nil
    55  }
    56  
    57  // EnsureDdevNetwork just creates or ensures the ddev network exists or
    58  // exits with fatal.
    59  func EnsureDdevNetwork() {
    60  	// ensure we have the fallback global ddev network
    61  	client := GetDockerClient()
    62  	err := EnsureNetwork(client, NetName)
    63  	if err != nil {
    64  		log.Fatalf("Failed to ensure docker network %s: %v", NetName, err)
    65  	}
    66  }
    67  
    68  // NetworkExists returns true if the named network exists
    69  // Mostly intended for tests
    70  func NetworkExists(netName string) bool {
    71  	// ensure we have docker network
    72  	client := GetDockerClient()
    73  	return NetExists(client, strings.ToLower(netName))
    74  }
    75  
    76  // RemoveNetwork removes the named docker network
    77  func RemoveNetwork(netName string) error {
    78  	client := GetDockerClient()
    79  	err := client.RemoveNetwork(netName)
    80  	return err
    81  }
    82  
    83  var DockerHost string
    84  var DockerContext string
    85  
    86  // GetDockerClient returns a docker client respecting the current docker context
    87  // but DOCKER_HOST gets priority
    88  func GetDockerClient() *docker.Client {
    89  	var err error
    90  
    91  	// This section is skipped if $DOCKER_HOST is set
    92  	if DockerHost == "" {
    93  		DockerContext, DockerHost, err = GetDockerContext()
    94  		// ddev --version may be called without docker client or context available, ignore err
    95  		if err != nil && len(os.Args) > 1 && os.Args[1] != "--version" && os.Args[1] != "hostname" {
    96  			util.Failed("Unable to get docker context: %v", err)
    97  		}
    98  		util.Debug("GetDockerClient: DockerContext=%s, DockerHost=%s", DockerContext, DockerHost)
    99  	}
   100  	// Respect DOCKER_HOST in case it's set, otherwise use host we got from context
   101  	if os.Getenv("DOCKER_HOST") == "" {
   102  		util.Debug("GetDockerClient: Setting DOCKER_HOST to '%s'", DockerHost)
   103  		_ = os.Setenv("DOCKER_HOST", DockerHost)
   104  	}
   105  	client, err := docker.NewClientFromEnv()
   106  	if err != nil {
   107  		output.UserOut.Warnf("could not get docker client. is docker running? error: %v", err)
   108  		// Use os.Exit instead of util.Failed() to avoid import cycle with util.
   109  		os.Exit(100)
   110  	}
   111  	return client
   112  }
   113  
   114  // GetDockerContext() returns the currently set docker context, host, and error
   115  func GetDockerContext() (string, string, error) {
   116  	context := ""
   117  	dockerHost := ""
   118  
   119  	// This is a cheap way of using docker contexts by running `docker context inspect`
   120  	// I would wish for something far better, but trying to transplant the code from
   121  	// docker/cli did not succeed. rfay 2021-12-16
   122  	// `docker context inspect` will already respect $DOCKER_CONTEXT so we don't have to do that.
   123  	contextInfo, err := ddevexec.RunHostCommand("docker", "context", "inspect", "-f", `{{ .Name }} {{ .Endpoints.docker.Host }}`)
   124  	if err != nil {
   125  		return "", "", fmt.Errorf("unable to run 'docker context inspect' - please make sure docker client is in path and up-to-date: %v", err)
   126  	}
   127  	contextInfo = strings.Trim(contextInfo, " \r\n")
   128  	util.Debug("GetDockerContext: contextInfo='%s'", contextInfo)
   129  	parts := strings.SplitN(contextInfo, " ", 2)
   130  	if len(parts) != 2 {
   131  		return "", "", fmt.Errorf("unable to run split docker context info %s: %v", contextInfo, err)
   132  	}
   133  	context = parts[0]
   134  	dockerHost = parts[1]
   135  	util.Debug("Using docker context %s (%v)", context, dockerHost)
   136  	return context, dockerHost, nil
   137  }
   138  
   139  // GetDockerHostID returns DOCKER_HOST but with all special characters removed
   140  // It stands in for docker context, but docker context name is not a reliable indicator
   141  func GetDockerHostID() string {
   142  	_, dockerHost, err := GetDockerContext()
   143  	if err != nil {
   144  		util.Warning("Unable to GetDockerContext: %v", err)
   145  	}
   146  	// Make it shorter so we don't hit mutagen 63-char limit
   147  	dockerHost = strings.TrimPrefix(dockerHost, "unix://")
   148  	dockerHost = strings.TrimSuffix(dockerHost, "docker.sock")
   149  	dockerHost = strings.Trim(dockerHost, "/.")
   150  	// Convert remaining descriptor to alphanumeric
   151  	reg, err := regexp.Compile("[^a-zA-Z0-9]+")
   152  	if err != nil {
   153  		log.Fatal(err)
   154  	}
   155  	alphaOnly := reg.ReplaceAllString(dockerHost, "-")
   156  	return alphaOnly
   157  }
   158  
   159  // InspectContainer returns the full result of inspection
   160  func InspectContainer(name string) (*docker.Container, error) {
   161  	client, err := docker.NewClientFromEnv()
   162  
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  	c, err := FindContainerByName(name)
   167  	if err != nil || c == nil {
   168  		return nil, err
   169  	}
   170  	x, err := client.InspectContainerWithOptions(docker.InspectContainerOptions{ID: c.ID})
   171  	return x, err
   172  }
   173  
   174  // FindContainerByName takes a container name and returns the container ID
   175  // If container is not found, returns nil with no error
   176  func FindContainerByName(name string) (*docker.APIContainers, error) {
   177  	client := GetDockerClient()
   178  
   179  	containers, err := client.ListContainers(docker.ListContainersOptions{
   180  		All:     true,
   181  		Filters: map[string][]string{"name": {name}},
   182  	})
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  	if len(containers) == 0 {
   187  		return nil, nil
   188  	}
   189  
   190  	// ListContainers can return partial matches. Make sure we only match the exact one
   191  	// we're after.
   192  	for _, c := range containers {
   193  		if c.Names[0] == "/"+name {
   194  			return &c, nil
   195  		}
   196  	}
   197  	return nil, nil
   198  }
   199  
   200  // GetContainerStateByName returns container state for the named container
   201  func GetContainerStateByName(name string) (string, error) {
   202  	container, err := FindContainerByName(name)
   203  	if err != nil || container == nil {
   204  		return "doesnotexist", fmt.Errorf("container %s does not exist", name)
   205  	}
   206  	if container.State == "running" {
   207  		return container.State, nil
   208  	}
   209  	return container.State, fmt.Errorf("container %s is in state=%s so can't be accessed", name, container.State)
   210  }
   211  
   212  // FindContainerByLabels takes a map of label names and values and returns any docker containers which match all labels.
   213  func FindContainerByLabels(labels map[string]string) (*docker.APIContainers, error) {
   214  	containers, err := FindContainersByLabels(labels)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  	if len(containers) > 0 {
   219  		return &containers[0], nil
   220  	}
   221  	return nil, nil
   222  }
   223  
   224  // GetDockerContainers returns a slice of all docker containers on the host system.
   225  func GetDockerContainers(allContainers bool) ([]docker.APIContainers, error) {
   226  	client := GetDockerClient()
   227  	containers, err := client.ListContainers(docker.ListContainersOptions{All: allContainers})
   228  	return containers, err
   229  }
   230  
   231  // FindContainersByLabels takes a map of label names and values and returns any docker containers which match all labels.
   232  // Explanation of the query:
   233  // * docs: https://docs.docker.com/engine/api/v1.23/
   234  // * Stack Overflow: https://stackoverflow.com/questions/28054203/docker-remote-api-filter-exited
   235  func FindContainersByLabels(labels map[string]string) ([]docker.APIContainers, error) {
   236  	if len(labels) < 1 {
   237  		return []docker.APIContainers{{}}, fmt.Errorf("the provided list of labels was empty")
   238  	}
   239  	filterList := []string{}
   240  	for k, v := range labels {
   241  		filterList = append(filterList, fmt.Sprintf("%s=%s", k, v))
   242  	}
   243  
   244  	client := GetDockerClient()
   245  	containers, err := client.ListContainers(docker.ListContainersOptions{
   246  		All:     true,
   247  		Filters: map[string][]string{"label": filterList},
   248  	})
   249  	if err != nil {
   250  		return nil, err
   251  	}
   252  	return containers, nil
   253  }
   254  
   255  // FindContainersWithLabel returns all containers with the given label
   256  // It ignores the value of the label, is only interested that the label exists.
   257  func FindContainersWithLabel(label string) ([]docker.APIContainers, error) {
   258  	client := GetDockerClient()
   259  	containers, err := client.ListContainers(docker.ListContainersOptions{
   260  		All:     true,
   261  		Filters: map[string][]string{"label": {label}},
   262  	})
   263  	if err != nil {
   264  		return nil, err
   265  	}
   266  	return containers, nil
   267  }
   268  
   269  // NetExists checks to see if the docker network for ddev exists.
   270  func NetExists(client *docker.Client, name string) bool {
   271  	nets, _ := client.ListNetworks()
   272  	for _, n := range nets {
   273  		if n.Name == name {
   274  			return true
   275  		}
   276  	}
   277  	return false
   278  }
   279  
   280  // ContainerWait provides a wait loop to check for a single container in "healthy" status.
   281  // waittime is in seconds.
   282  // This is modeled on https://gist.github.com/ngauthier/d6e6f80ce977bedca601
   283  // Returns logoutput, error, returns error if not "healthy"
   284  func ContainerWait(waittime int, labels map[string]string) (string, error) {
   285  
   286  	durationWait := time.Duration(waittime) * time.Second
   287  	timeoutChan := time.NewTimer(durationWait)
   288  	tickChan := time.NewTicker(500 * time.Millisecond)
   289  	defer tickChan.Stop()
   290  	defer timeoutChan.Stop()
   291  
   292  	status := ""
   293  
   294  	for {
   295  		select {
   296  		case <-timeoutChan.C:
   297  			_ = timeoutChan.Stop()
   298  			return "", fmt.Errorf("health check timed out after %v: labels %v timed out without becoming healthy, status=%v", durationWait, labels, status)
   299  
   300  		case <-tickChan.C:
   301  			container, err := FindContainerByLabels(labels)
   302  			if err != nil || container == nil {
   303  				return "", fmt.Errorf("failed to query container labels=%v: %v", labels, err)
   304  			}
   305  			health, logOutput := GetContainerHealth(container)
   306  
   307  			switch health {
   308  			case "healthy":
   309  				return logOutput, nil
   310  			case "unhealthy":
   311  				return logOutput, fmt.Errorf("container %s unhealthy: %s", container.Names[0], logOutput)
   312  			case "exited":
   313  				service := container.Labels["com.docker.compose.service"]
   314  				suggestedCommand := fmt.Sprintf("ddev logs -s %s", service)
   315  				if service == "ddev-router" || service == "ddev-ssh-agent" {
   316  					suggestedCommand = fmt.Sprintf("docker logs %s", service)
   317  				}
   318  				return logOutput, fmt.Errorf("container exited, please use '%s' to find out why it failed", suggestedCommand)
   319  			}
   320  		}
   321  	}
   322  
   323  	// We should never get here.
   324  	//nolint: govet
   325  	return "", fmt.Errorf("inappropriate break out of for loop in ContainerWait() waiting for container labels %v", labels)
   326  }
   327  
   328  // ContainersWait provides a wait loop to check for multiple containers in "healthy" status.
   329  // waittime is in seconds.
   330  // Returns logoutput, error, returns error if not "healthy"
   331  func ContainersWait(waittime int, labels map[string]string) error {
   332  
   333  	timeoutChan := time.After(time.Duration(waittime) * time.Second)
   334  	tickChan := time.NewTicker(500 * time.Millisecond)
   335  	defer tickChan.Stop()
   336  
   337  	status := ""
   338  
   339  	for {
   340  		select {
   341  		case <-timeoutChan:
   342  			desc := ""
   343  			containers, err := FindContainersByLabels(labels)
   344  			if err == nil && containers != nil {
   345  				for _, c := range containers {
   346  					health, _ := GetContainerHealth(&c)
   347  					if health != "healthy" {
   348  						n := strings.TrimPrefix(c.Names[0], "/")
   349  						desc = desc + fmt.Sprintf(" %s:%s - more info with `docker inspect --format \"{{json .State.Health }}\" %s`", n, health, n)
   350  					}
   351  				}
   352  			}
   353  			return fmt.Errorf("health check timed out: labels %v timed out without becoming healthy, status=%v, detail=%s ", labels, status, desc)
   354  
   355  		case <-tickChan.C:
   356  			containers, err := FindContainersByLabels(labels)
   357  			allHealthy := true
   358  			for _, c := range containers {
   359  				if err != nil || containers == nil {
   360  					return fmt.Errorf("failed to query container labels=%v: %v", labels, err)
   361  				}
   362  				health, logOutput := GetContainerHealth(&c)
   363  
   364  				switch health {
   365  				case "healthy":
   366  					continue
   367  				case "unhealthy":
   368  					return fmt.Errorf("container %s is unhealthy: %s", c.Names[0], logOutput)
   369  				case "exited":
   370  					service := c.Labels["com.docker.compose.service"]
   371  					suggestedCommand := fmt.Sprintf("ddev logs -s %s", service)
   372  					if service == "ddev-router" || service == "ddev-ssh-agent" {
   373  						suggestedCommand = fmt.Sprintf("docker logs %s", service)
   374  					}
   375  					return fmt.Errorf("container '%s' exited, please use '%s' to find out why it failed", service, suggestedCommand)
   376  				default:
   377  					allHealthy = false
   378  				}
   379  			}
   380  			if allHealthy {
   381  				return nil
   382  			}
   383  		}
   384  	}
   385  
   386  	// We should never get here.
   387  	//nolint: govet
   388  	return fmt.Errorf("inappropriate break out of for loop in ContainerWait() waiting for container labels %v", labels)
   389  }
   390  
   391  // ContainerWaitLog provides a wait loop to check for container in "healthy" status.
   392  // with a given log output
   393  // timeout is in seconds.
   394  // This is modeled on https://gist.github.com/ngauthier/d6e6f80ce977bedca601
   395  // Returns logoutput, error, returns error if not "healthy"
   396  func ContainerWaitLog(waittime int, labels map[string]string, expectedLog string) (string, error) {
   397  
   398  	timeoutChan := time.After(time.Duration(waittime) * time.Second)
   399  	tickChan := time.NewTicker(500 * time.Millisecond)
   400  	defer tickChan.Stop()
   401  
   402  	status := ""
   403  
   404  	for {
   405  		select {
   406  		case <-timeoutChan:
   407  			return "", fmt.Errorf("health check timed out: labels %v timed out without becoming healthy, status=%v", labels, status)
   408  
   409  		case <-tickChan.C:
   410  			container, err := FindContainerByLabels(labels)
   411  			if err != nil || container == nil {
   412  				return "", fmt.Errorf("failed to query container labels=%v: %v", labels, err)
   413  			}
   414  			status, logOutput := GetContainerHealth(container)
   415  
   416  			switch {
   417  			case status == "healthy" && expectedLog == logOutput:
   418  				return logOutput, nil
   419  			case status == "unhealthy":
   420  				return logOutput, fmt.Errorf("container %s unhealthy: %s", container.Names[0], logOutput)
   421  			case status == "exited":
   422  				service := container.Labels["com.docker.compose.service"]
   423  				return logOutput, fmt.Errorf("container exited, please use 'ddev logs -s %s` to find out why it failed", service)
   424  			}
   425  		}
   426  	}
   427  
   428  	// We should never get here.
   429  	//nolint: govet
   430  	return "", fmt.Errorf("inappropriate break out of for loop in ContainerWaitLog() waiting for container labels %v", labels)
   431  }
   432  
   433  // ContainerName returns the container's human readable name.
   434  func ContainerName(container docker.APIContainers) string {
   435  	return container.Names[0][1:]
   436  }
   437  
   438  // GetContainerHealth retrieves the health status of a given container.
   439  // returns status, most-recent-log
   440  func GetContainerHealth(container *docker.APIContainers) (string, string) {
   441  	if container == nil {
   442  		return "no container", ""
   443  	}
   444  
   445  	// If the container is not running, then return exited as the health.
   446  	// "exited" means stopped.
   447  	if container.State == "exited" || container.State == "restarting" {
   448  		return container.State, ""
   449  	}
   450  
   451  	client := GetDockerClient()
   452  	inspect, err := client.InspectContainerWithOptions(docker.InspectContainerOptions{
   453  		ID: container.ID,
   454  	})
   455  	if err != nil || inspect == nil {
   456  		output.UserOut.Warnf("Error getting container to inspect: %v", err)
   457  		return "", ""
   458  	}
   459  
   460  	logOutput := ""
   461  	status := inspect.State.Health.Status
   462  	// The last log is the most recent
   463  	if inspect.State.Health.Status != "" {
   464  		numLogs := len(inspect.State.Health.Log)
   465  		if numLogs > 0 {
   466  			logOutput = inspect.State.Health.Log[numLogs-1].Output
   467  		}
   468  	} else {
   469  		// Some containers may not have a healthcheck. In that case
   470  		// we use State to determine health
   471  		switch inspect.State.Status {
   472  		case "running":
   473  			status = "healthy"
   474  		case "exited":
   475  			status = "exited"
   476  		}
   477  	}
   478  
   479  	return status, logOutput
   480  }
   481  
   482  // ComposeWithStreams executes a docker-compose command but allows the caller to specify
   483  // stdin/stdout/stderr
   484  func ComposeWithStreams(composeFiles []string, stdin io.Reader, stdout io.Writer, stderr io.Writer, action ...string) error {
   485  	var arg []string
   486  
   487  	runTime := util.TimeTrack(time.Now(), "dockerutil.ComposeWithStreams")
   488  	defer runTime()
   489  
   490  	_, err := DownloadDockerComposeIfNeeded()
   491  	if err != nil {
   492  		return err
   493  	}
   494  
   495  	for _, file := range composeFiles {
   496  		arg = append(arg, "-f")
   497  		arg = append(arg, file)
   498  	}
   499  
   500  	arg = append(arg, action...)
   501  
   502  	path, err := globalconfig.GetDockerComposePath()
   503  	if err != nil {
   504  		return err
   505  	}
   506  	proc := exec.Command(path, arg...)
   507  	proc.Stdout = stdout
   508  	proc.Stdin = stdin
   509  	proc.Stderr = stderr
   510  
   511  	err = proc.Run()
   512  	return err
   513  }
   514  
   515  // ComposeCmd executes docker-compose commands via shell.
   516  // returns stdout, stderr, error/nil
   517  func ComposeCmd(composeFiles []string, action ...string) (string, string, error) {
   518  	var arg []string
   519  	var stdout bytes.Buffer
   520  	var stderr string
   521  
   522  	_, err := DownloadDockerComposeIfNeeded()
   523  	if err != nil {
   524  		return "", "", err
   525  	}
   526  
   527  	for _, file := range composeFiles {
   528  		arg = append(arg, "-f", file)
   529  	}
   530  
   531  	arg = append(arg, action...)
   532  
   533  	path, err := globalconfig.GetDockerComposePath()
   534  	if err != nil {
   535  		return "", "", err
   536  	}
   537  	proc := exec.Command(path, arg...)
   538  	proc.Stdout = &stdout
   539  	proc.Stdin = os.Stdin
   540  
   541  	stderrPipe, err := proc.StderrPipe()
   542  	if err != nil {
   543  		return "", "", fmt.Errorf("Failed to proc.StderrPipe(): %v", err)
   544  	}
   545  
   546  	if err = proc.Start(); err != nil {
   547  		return "", "", fmt.Errorf("Failed to exec docker-compose: %v", err)
   548  	}
   549  
   550  	// read command's stdout line by line
   551  	in := bufio.NewScanner(stderrPipe)
   552  
   553  	// Ignore chatty things from docker-compose like:
   554  	// Container (or Volume) ... Creating or Created or Stopping or Starting or Removing
   555  	// Container Stopped or Created
   556  	// No resource found to remove (when doing a stop and no project exists)
   557  	ignoreRegex := "(^ *(Network|Container|Volume) .* (Creat|Start|Stopp|Remov)ing$|^Container .*(Stopp|Creat)(ed|ing)$|Warning: No resource found to remove$|Pulling fs layer|Waiting|Downloading|Extracting|Verifying Checksum|Download complete|Pull complete)"
   558  	downRE, err := regexp.Compile(ignoreRegex)
   559  	if err != nil {
   560  		util.Warning("failed to compile regex %v: %v", ignoreRegex, err)
   561  	}
   562  
   563  	for in.Scan() {
   564  		line := in.Text()
   565  		if len(stderr) > 0 {
   566  			stderr = stderr + "\n"
   567  		}
   568  		stderr = stderr + line
   569  		line = strings.Trim(line, "\n\r")
   570  		switch {
   571  		case downRE.MatchString(line):
   572  			break
   573  		default:
   574  			output.UserOut.Println(line)
   575  		}
   576  	}
   577  
   578  	err = proc.Wait()
   579  	if err != nil {
   580  		return stdout.String(), stderr, fmt.Errorf("ComposeCmd failed to run 'COMPOSE_PROJECT_NAME=%s docker-compose %v', action='%v', err='%v', stdout='%s', stderr='%s'", os.Getenv("COMPOSE_PROJECT_NAME"), strings.Join(arg, " "), action, err, stdout.String(), stderr)
   581  	}
   582  	return stdout.String(), stderr, nil
   583  }
   584  
   585  // GetAppContainers retrieves docker containers for a given sitename.
   586  func GetAppContainers(sitename string) ([]docker.APIContainers, error) {
   587  	label := map[string]string{"com.ddev.site-name": sitename}
   588  	containers, err := FindContainersByLabels(label)
   589  	if err != nil {
   590  		return containers, err
   591  	}
   592  	return containers, nil
   593  }
   594  
   595  // GetContainerEnv returns the value of a given environment variable from a given container.
   596  func GetContainerEnv(key string, container docker.APIContainers) string {
   597  	client := GetDockerClient()
   598  	inspect, err := client.InspectContainerWithOptions(docker.InspectContainerOptions{
   599  		ID: container.ID,
   600  	})
   601  	if err == nil {
   602  		envVars := inspect.Config.Env
   603  
   604  		for _, env := range envVars {
   605  			if strings.HasPrefix(env, key) {
   606  				return strings.TrimPrefix(env, key+"=")
   607  			}
   608  		}
   609  	}
   610  	return ""
   611  }
   612  
   613  // CheckDockerVersion determines if the docker version of the host system meets the provided version
   614  // constraints. See https://godoc.org/github.com/Masterminds/semver#hdr-Checking_Version_Constraints
   615  // for examples defining version constraints.
   616  func CheckDockerVersion(versionConstraint string) error {
   617  	runTime := util.TimeTrack(time.Now(), "CheckDockerVersion()")
   618  	defer runTime()
   619  
   620  	currentVersion, err := GetDockerVersion()
   621  	if err != nil {
   622  		return fmt.Errorf("no docker")
   623  	}
   624  	// If docker version has "_ce", remove it. This happens on OpenSUSE Tumbleweed at least
   625  	currentVersion = strings.TrimSuffix(currentVersion, "_ce")
   626  	dockerVersion, err := semver.NewVersion(currentVersion)
   627  	if err != nil {
   628  		return err
   629  	}
   630  
   631  	// See if they're using broken docker desktop on linux
   632  	if runtime.GOOS == "linux" && !IsWSL2() {
   633  		client := GetDockerClient()
   634  		info, err := client.Info()
   635  		if err != nil {
   636  			return fmt.Errorf("unable to get docker info: %v", err)
   637  		}
   638  		if info.Name == "docker-desktop" {
   639  			return fmt.Errorf("Docker Desktop on Linux is not yet compatible with DDEV")
   640  		}
   641  	}
   642  
   643  	constraint, err := semver.NewConstraint(versionConstraint)
   644  	if err != nil {
   645  		return err
   646  	}
   647  
   648  	match, errs := constraint.Validate(dockerVersion)
   649  	if !match {
   650  		if len(errs) <= 1 {
   651  			return errs[0]
   652  		}
   653  
   654  		msgs := "\n"
   655  		for _, err := range errs {
   656  			msgs = fmt.Sprint(msgs, err, "\n")
   657  		}
   658  		return fmt.Errorf(msgs)
   659  	}
   660  	return nil
   661  }
   662  
   663  // CheckDockerCompose determines if docker-compose is present and executable on the host system. This
   664  // relies on docker-compose being somewhere in the user's $PATH.
   665  func CheckDockerCompose() error {
   666  	runTime := util.TimeTrack(time.Now(), "CheckDockerComposeVersion()")
   667  	defer runTime()
   668  
   669  	_, err := DownloadDockerComposeIfNeeded()
   670  	if err != nil {
   671  		return err
   672  	}
   673  	versionConstraint := DockerComposeVersionConstraint
   674  
   675  	v, err := GetDockerComposeVersion()
   676  	if err != nil {
   677  		return err
   678  	}
   679  	dockerComposeVersion, err := semver.NewVersion(v)
   680  	if err != nil {
   681  		return err
   682  	}
   683  
   684  	constraint, err := semver.NewConstraint(versionConstraint)
   685  	if err != nil {
   686  		return err
   687  	}
   688  
   689  	match, errs := constraint.Validate(dockerComposeVersion)
   690  	if !match {
   691  		if len(errs) <= 1 {
   692  			return errs[0]
   693  		}
   694  
   695  		msgs := "\n"
   696  		for _, err := range errs {
   697  			msgs = fmt.Sprint(msgs, err, "\n")
   698  		}
   699  		return fmt.Errorf(msgs)
   700  	}
   701  
   702  	return nil
   703  }
   704  
   705  // GetPublishedPort returns the published port for a given private port.
   706  func GetPublishedPort(privatePort int64, container docker.APIContainers) int {
   707  	for _, port := range container.Ports {
   708  		if port.PrivatePort == privatePort {
   709  			return int(port.PublicPort)
   710  		}
   711  	}
   712  	return 0
   713  }
   714  
   715  // CheckForHTTPS determines if a container has the HTTPS_EXPOSE var
   716  // set to route 443 traffic to 80
   717  func CheckForHTTPS(container docker.APIContainers) bool {
   718  	env := GetContainerEnv("HTTPS_EXPOSE", container)
   719  	if env != "" && strings.Contains(env, "443:80") {
   720  		return true
   721  	}
   722  	return false
   723  }
   724  
   725  var dockerHostRawURL string
   726  var DockerIP string
   727  
   728  // GetDockerIP returns either the default Docker IP address (127.0.0.1)
   729  // or the value as configured by $DOCKER_HOST (if DOCKER_HOST is an tcp:// URL)
   730  func GetDockerIP() (string, error) {
   731  	if DockerIP == "" {
   732  		DockerIP = "127.0.0.1"
   733  		dockerHostRawURL = os.Getenv("DOCKER_HOST")
   734  		// If DOCKER_HOST is empty, then the client hasn't been initialized
   735  		// from the docker context
   736  		if dockerHostRawURL == "" {
   737  			_ = GetDockerClient()
   738  			dockerHostRawURL = os.Getenv("DOCKER_HOST")
   739  		}
   740  		if dockerHostRawURL != "" {
   741  			dockerHostURL, err := url.Parse(dockerHostRawURL)
   742  			if err != nil {
   743  				return "", fmt.Errorf("failed to parse $DOCKER_HOST=%s: %v", dockerHostRawURL, err)
   744  			}
   745  			hostPart := dockerHostURL.Hostname()
   746  			if hostPart != "" {
   747  				// Check to see if the hostname we found is an IP address
   748  				addr := net.ParseIP(hostPart)
   749  				if addr == nil {
   750  					// If it wasn't an IP address, look it up to get IP address
   751  					ip, err := net.LookupHost(hostPart)
   752  					if err == nil && len(ip) > 0 {
   753  						hostPart = ip[0]
   754  					} else {
   755  						return "", fmt.Errorf("failed to look up IP address for $DOCKER_HOST=%s, hostname=%s: %v", dockerHostRawURL, hostPart, err)
   756  					}
   757  				}
   758  				DockerIP = hostPart
   759  			}
   760  		}
   761  	}
   762  	return DockerIP, nil
   763  }
   764  
   765  // RunSimpleContainer runs a container (non-daemonized) and captures the stdout/stderr.
   766  // It will block, so not to be run on a container whose entrypoint or cmd might hang or run too long.
   767  // This should be the equivalent of something like
   768  // docker run -t -u '%s:%s' -e SNAPSHOT_NAME='%s' -v '%s:/mnt/ddev_config' -v '%s:/var/lib/mysql' --rm --entrypoint=/migrate_file_to_volume.sh %s:%s"
   769  // Example code from https://gist.github.com/fsouza/b0bf3043827f8e39c4589e88cec067d8
   770  // Returns containerID, output, error
   771  func RunSimpleContainer(image string, name string, cmd []string, entrypoint []string, env []string, binds []string, uid string, removeContainerAfterRun bool, detach bool, labels map[string]string) (containerID string, output string, returnErr error) {
   772  	client := GetDockerClient()
   773  
   774  	// Ensure image string includes a tag
   775  	imageChunks := strings.Split(image, ":")
   776  	if len(imageChunks) == 1 {
   777  		// Image does not specify tag
   778  		return "", "", fmt.Errorf("image name must specify tag: %s", image)
   779  	}
   780  
   781  	if tag := imageChunks[len(imageChunks)-1]; len(tag) == 0 {
   782  		// Image specifies malformed tag (ends with ':')
   783  		return "", "", fmt.Errorf("malformed tag provided: %s", image)
   784  	}
   785  
   786  	existsLocally, err := ImageExistsLocally(image)
   787  	if err != nil {
   788  		return "", "", fmt.Errorf("failed to check if image %s is available locally: %v", image, err)
   789  	}
   790  
   791  	if !existsLocally {
   792  		pullErr := Pull(image)
   793  		if pullErr != nil {
   794  			return "", "", fmt.Errorf("failed to pull image %s: %v", image, pullErr)
   795  		}
   796  	}
   797  
   798  	// Windows 10 Docker toolbox won't handle a bind mount like C:\..., so must convert to /c/...
   799  	if runtime.GOOS == "windows" {
   800  		for i := range binds {
   801  			binds[i] = strings.Replace(binds[i], `\`, `/`, -1)
   802  			if strings.Index(binds[i], ":") == 1 {
   803  				binds[i] = strings.Replace(binds[i], ":", "", 1)
   804  				binds[i] = "/" + binds[i]
   805  				// And amazingly, the drive letter must be lower-case.
   806  				re := regexp.MustCompile("^/[A-Z]/")
   807  				driveLetter := re.FindString(binds[i])
   808  				if len(driveLetter) == 3 {
   809  					binds[i] = strings.TrimPrefix(binds[i], driveLetter)
   810  					binds[i] = strings.ToLower(driveLetter) + binds[i]
   811  				}
   812  
   813  			}
   814  		}
   815  	}
   816  
   817  	options := docker.CreateContainerOptions{
   818  		Name: name,
   819  		Config: &docker.Config{
   820  			Image:        image,
   821  			Cmd:          cmd,
   822  			Env:          env,
   823  			User:         uid,
   824  			Labels:       labels,
   825  			Entrypoint:   entrypoint,
   826  			AttachStderr: true,
   827  			AttachStdout: true,
   828  		},
   829  		HostConfig: &docker.HostConfig{
   830  			Binds: binds,
   831  		},
   832  	}
   833  
   834  	if runtime.GOOS == "linux" && !IsDockerDesktop() {
   835  		options.HostConfig.ExtraHosts = []string{"host.docker.internal:host-gateway"}
   836  	}
   837  	container, err := client.CreateContainer(options)
   838  	if err != nil {
   839  		return "", "", fmt.Errorf("failed to create/start docker container (%v):%v", options, err)
   840  	}
   841  
   842  	if removeContainerAfterRun {
   843  		// nolint: errcheck
   844  		defer RemoveContainer(container.ID, 20)
   845  	}
   846  	err = client.StartContainer(container.ID, nil)
   847  	if err != nil {
   848  		return container.ID, "", fmt.Errorf("failed to StartContainer: %v", err)
   849  	}
   850  	exitCode := 0
   851  	if !detach {
   852  		exitCode, err = client.WaitContainer(container.ID)
   853  		if err != nil {
   854  			return container.ID, "", fmt.Errorf("failed to WaitContainer: %v", err)
   855  		}
   856  	}
   857  
   858  	// Get logs so we can report them if exitCode failed
   859  	var stdout bytes.Buffer
   860  	err = client.Logs(docker.LogsOptions{
   861  		Stdout:       true,
   862  		Stderr:       true,
   863  		Container:    container.ID,
   864  		OutputStream: &stdout,
   865  		ErrorStream:  &stdout,
   866  	})
   867  	if err != nil {
   868  		return container.ID, "", fmt.Errorf("failed to get Logs(): %v", err)
   869  	}
   870  
   871  	// This is the exitCode from the client.WaitContainer()
   872  	if exitCode != 0 {
   873  		return container.ID, stdout.String(), fmt.Errorf("container run failed with exit code %d", exitCode)
   874  	}
   875  
   876  	return container.ID, stdout.String(), nil
   877  }
   878  
   879  // RemoveContainer stops and removes a container
   880  func RemoveContainer(id string, timeout uint) error {
   881  	client := GetDockerClient()
   882  
   883  	err := client.RemoveContainer(docker.RemoveContainerOptions{ID: id, Force: true})
   884  	return err
   885  }
   886  
   887  // RestartContainer stops and removes a container
   888  func RestartContainer(id string, timeout uint) error {
   889  	client := GetDockerClient()
   890  
   891  	err := client.RestartContainer(id, 20)
   892  	return err
   893  }
   894  
   895  // RemoveContainersByLabels removes all containers that match a set of labels
   896  func RemoveContainersByLabels(labels map[string]string) error {
   897  	client := GetDockerClient()
   898  	containers, err := FindContainersByLabels(labels)
   899  	if err != nil {
   900  		return err
   901  	}
   902  	if containers == nil {
   903  		return nil
   904  	}
   905  	for _, c := range containers {
   906  		err = client.RemoveContainer(docker.RemoveContainerOptions{ID: c.ID, Force: true})
   907  		if err != nil {
   908  			return err
   909  		}
   910  	}
   911  	return nil
   912  }
   913  
   914  // ImageExistsLocally determines if an image is available locally.
   915  func ImageExistsLocally(imageName string) (bool, error) {
   916  	client := GetDockerClient()
   917  
   918  	// If inspect succeeeds, we have an image.
   919  	_, err := client.InspectImage(imageName)
   920  	if err == nil {
   921  		return true, nil
   922  	}
   923  	return false, nil
   924  }
   925  
   926  // Pull pulls image if it doesn't exist locally.
   927  func Pull(imageName string) error {
   928  	exists, err := ImageExistsLocally(imageName)
   929  	if err != nil {
   930  		return err
   931  	}
   932  	if exists {
   933  		return nil
   934  	}
   935  	cmd := exec.Command("docker", "pull", imageName)
   936  	cmd.Stdout = os.Stdout
   937  	cmd.Stderr = os.Stderr
   938  	err = cmd.Run()
   939  	return err
   940  }
   941  
   942  // GetExposedContainerPorts takes a container pointer and returns an array
   943  // of exposed ports (and error)
   944  func GetExposedContainerPorts(containerID string) ([]string, error) {
   945  	client := GetDockerClient()
   946  	inspectInfo, err := client.InspectContainerWithOptions(docker.InspectContainerOptions{
   947  		ID: containerID,
   948  	})
   949  
   950  	if err != nil {
   951  		return nil, err
   952  	}
   953  
   954  	portMap := map[string]bool{}
   955  	for _, portMapping := range inspectInfo.NetworkSettings.Ports {
   956  		if portMapping != nil && len(portMapping) > 0 {
   957  			for _, item := range portMapping {
   958  				portMap[item.HostPort] = true
   959  			}
   960  		}
   961  	}
   962  	ports := []string{}
   963  	for k := range portMap {
   964  		ports = append(ports, k)
   965  	}
   966  	sort.Slice(ports, func(i, j int) bool {
   967  		return ports[i] < ports[j]
   968  	})
   969  	return ports, nil
   970  }
   971  
   972  // MassageWindowsHostMountpoint changes C:/path/to/something to //c/path/to/something
   973  // THis is required for docker bind mounts on docker toolbox.
   974  // Sadly, if we have a Windows drive name, it has to be converted from C:/ to //c for Win10Home/Docker toolbox
   975  func MassageWindowsHostMountpoint(mountPoint string) string {
   976  	if string(mountPoint[1]) == ":" {
   977  		pathPortion := strings.Replace(mountPoint[2:], `\`, "/", -1)
   978  		drive := strings.ToLower(string(mountPoint[0]))
   979  		mountPoint = "/" + drive + pathPortion
   980  	}
   981  	return mountPoint
   982  }
   983  
   984  // MassageWindowsNFSMount changes C:\Path\to\something to /c/Path/to/something
   985  func MassageWindowsNFSMount(mountPoint string) string {
   986  	if string(mountPoint[1]) == ":" {
   987  		pathPortion := strings.Replace(mountPoint[2:], `\`, "/", -1)
   988  		drive := string(mountPoint[0])
   989  		// Because we use $HOME to get home in exports, and $HOME has /c/Users/xxx
   990  		// change the drive to lower case.
   991  		mountPoint = "/" + strings.ToLower(drive) + pathPortion
   992  	}
   993  	return mountPoint
   994  }
   995  
   996  // RemoveVolume removes named volume. Does not throw error if the volume did not exist.
   997  func RemoveVolume(volumeName string) error {
   998  	client := GetDockerClient()
   999  	if _, err := client.InspectVolume(volumeName); err == nil {
  1000  		err := client.RemoveVolumeWithOptions(docker.RemoveVolumeOptions{Name: volumeName})
  1001  		if err != nil {
  1002  			if err.Error() == "volume in use and cannot be removed" {
  1003  				containers, err := client.ListContainers(docker.ListContainersOptions{
  1004  					All:     true,
  1005  					Filters: map[string][]string{"volume": {volumeName}},
  1006  				})
  1007  				// Get names of containers which are still using the volume.
  1008  				var containerNames []string
  1009  				if err == nil {
  1010  					for _, container := range containers {
  1011  						// Skip first character, it's a slash.
  1012  						containerNames = append(containerNames, container.Names[0][1:])
  1013  					}
  1014  					var containerNamesString = strings.Join(containerNames, " ")
  1015  					return fmt.Errorf("Docker volume '%s' is in use by one or more containers and cannot be removed. Use 'docker rm -f %s' to remove them", volumeName, containerNamesString)
  1016  				}
  1017  				return fmt.Errorf("Docker volume '%s' is in use by a container and cannot be removed. Use 'docker rm -f $(docker ps -aq)' to remove all containers", volumeName)
  1018  			}
  1019  			return err
  1020  		}
  1021  	}
  1022  	return nil
  1023  }
  1024  
  1025  // VolumeExists checks to see if the named volume exists.
  1026  func VolumeExists(volumeName string) bool {
  1027  	client := GetDockerClient()
  1028  	_, err := client.InspectVolume(volumeName)
  1029  	if err != nil {
  1030  		return false
  1031  	}
  1032  	return true
  1033  }
  1034  
  1035  // VolumeLabels returns map of labels found on volume.
  1036  func VolumeLabels(volumeName string) (map[string]string, error) {
  1037  	client := GetDockerClient()
  1038  	v, err := client.InspectVolume(volumeName)
  1039  	if err != nil {
  1040  		return nil, err
  1041  	}
  1042  	return v.Labels, nil
  1043  }
  1044  
  1045  // CreateVolume creates a docker volume
  1046  func CreateVolume(volumeName string, driver string, driverOpts map[string]string, labels map[string]string) (volume *docker.Volume, err error) {
  1047  	client := GetDockerClient()
  1048  	volume, err = client.CreateVolume(docker.CreateVolumeOptions{Name: volumeName, Labels: labels, Driver: driver, DriverOpts: driverOpts})
  1049  	return volume, err
  1050  }
  1051  
  1052  // GetHostDockerInternalIP returns either "" (will use the hostname as is)
  1053  // (for Docker Desktop on macOS and Windows with WSL2) or a usable IP address
  1054  // But there are many cases to handle
  1055  // Linux classic installation
  1056  // Gitpod (the Linux technique does not work during prebuild)
  1057  // WSL2 with Docker-ce installed inside
  1058  // WSL2 with PhpStorm or vscode running inside WSL2
  1059  // And it matters whether they're running IDE inside. With docker-inside-wsl2, the bridge docker0 is what we want
  1060  // It's also possible to run vscode Language Server inside the web container, in which case host.docker.internal
  1061  // should actually be 127.0.0.1
  1062  // Inside WSL2, the way to access an app like PhpStorm running on the Windows side is described
  1063  // in https://learn.microsoft.com/en-us/windows/wsl/networking#accessing-windows-networking-apps-from-linux-host-ip
  1064  // and it involves parsing /etc/resolv.conf.
  1065  func GetHostDockerInternalIP() (string, error) {
  1066  	hostDockerInternal := ""
  1067  
  1068  	switch {
  1069  	case nodeps.IsIPAddress(globalconfig.DdevGlobalConfig.XdebugIDELocation):
  1070  		// If the IDE is actually listening inside container, then localhost/127.0.0.1 should work.
  1071  		hostDockerInternal = globalconfig.DdevGlobalConfig.XdebugIDELocation
  1072  
  1073  	case globalconfig.DdevGlobalConfig.XdebugIDELocation == globalconfig.XdebugIDELocationContainer:
  1074  		// If the IDE is actually listening inside container, then localhost/127.0.0.1 should work.
  1075  		hostDockerInternal = "127.0.0.1"
  1076  
  1077  	case IsColima():
  1078  		// Lima just specifies this as a named explicit IP address at this time
  1079  		// see https://github.com/lima-vm/lima/blob/master/docs/network.md#host-ip-19216852
  1080  		hostDockerInternal = "192.168.5.2"
  1081  
  1082  	// Gitpod has docker 20.10+ so the docker-compose has already gotten the host-gateway
  1083  	case nodeps.IsGitpod():
  1084  		break
  1085  	case nodeps.IsCodespaces():
  1086  		break
  1087  
  1088  	case IsWSL2() && IsDockerDesktop():
  1089  		// If IDE is on Windows, return; we don't have to do anything.
  1090  		break
  1091  
  1092  	case IsWSL2() && globalconfig.DdevGlobalConfig.XdebugIDELocation == globalconfig.XdebugIDELocationWSL2:
  1093  		// If IDE is inside WSL2 then the normal linux processing should work
  1094  		break
  1095  
  1096  	case IsWSL2() && !IsDockerDesktop():
  1097  		// If IDE is on Windows, we have to parse /etc/resolv.conf
  1098  		hostDockerInternal = wsl2ResolvConfNameserver()
  1099  
  1100  	// Docker on linux doesn't define host.docker.internal
  1101  	// so we need to go get the bridge IP address
  1102  	// Docker Desktop) defines host.docker.internal itself.
  1103  	case runtime.GOOS == "linux":
  1104  		// In docker 20.10+, host.docker.internal is already taken care of by extra_hosts in docker-compose
  1105  		break
  1106  	}
  1107  
  1108  	return hostDockerInternal, nil
  1109  }
  1110  
  1111  // GetNFSServerAddr gets the addrss that can be used for the NFS server.
  1112  // It's almost the same as GetDockerHostInternalIP() but we have
  1113  // to get the actual addr in the case of linux; still, linux rarely
  1114  // is used with NFS. Returns "host.docker.internal" by default (not empty)
  1115  func GetNFSServerAddr() (string, error) {
  1116  	nfsAddr := "host.docker.internal"
  1117  
  1118  	switch {
  1119  	case IsColima():
  1120  		// Lima just specifies this as a named explicit IP address at this time
  1121  		// see https://github.com/lima-vm/lima/blob/master/docs/network.md#host-ip-19216852
  1122  		nfsAddr = "192.168.5.2"
  1123  
  1124  	// Gitpod has docker 20.10+ so the docker-compose has already gotten the host-gateway
  1125  	// However, NFS will never be used on gitpod.
  1126  	case nodeps.IsGitpod():
  1127  		break
  1128  	case nodeps.IsCodespaces():
  1129  		break
  1130  
  1131  	case IsWSL2() && IsDockerDesktop():
  1132  		// If IDE is on Windows, return; we don't have to do anything.
  1133  		break
  1134  
  1135  	case IsWSL2() && !IsDockerDesktop():
  1136  		// If IDE is on Windows, we have to parse /etc/resolv.conf
  1137  		// Else it will be fine, we can fallthrough to the linux version
  1138  		nfsAddr = wsl2ResolvConfNameserver()
  1139  
  1140  	// Docker on linux doesn't define host.docker.internal
  1141  	// so we need to go get the bridge IP address
  1142  	// Docker Desktop) defines host.docker.internal itself.
  1143  	case runtime.GOOS == "linux":
  1144  		// look up info from the bridge network
  1145  		// We can't use the docker host because that's for inside the container,
  1146  		// and this is for setting up the network interface
  1147  		client := GetDockerClient()
  1148  		n, err := client.NetworkInfo("bridge")
  1149  		if err != nil {
  1150  			return "", err
  1151  		}
  1152  		if len(n.IPAM.Config) > 0 {
  1153  			if n.IPAM.Config[0].Gateway != "" {
  1154  				nfsAddr = n.IPAM.Config[0].Gateway
  1155  			} else {
  1156  				util.Warning("Unable to determine docker bridge gateway - no gateway")
  1157  			}
  1158  		}
  1159  	}
  1160  
  1161  	return nfsAddr, nil
  1162  }
  1163  
  1164  // wsl2ResolvConfNameserver parses /etc/resolv.conf to get the nameserver,
  1165  // which is the only documented way to know how to connect to the host
  1166  // to connect to PhpStorm or other IDE listening there. Or for other apps.
  1167  func wsl2ResolvConfNameserver() string {
  1168  	if IsWSL2() {
  1169  		isAuto, err := fileutil.FgrepStringInFile("/etc/resolv.conf", "automatically generated by WSL")
  1170  		if err != nil || !isAuto {
  1171  			util.Warning("unable to determine WSL2 host.docker.internal because /etc/resolv.conf is not available or not auto-generated")
  1172  			return ""
  1173  		}
  1174  		// We just grepped it so no need to check error
  1175  		etcResolv, _ := fileutil.ReadFileIntoString("/etc/resolv.conf")
  1176  
  1177  		nameserverRegex := regexp.MustCompile(`nameserver *([0-9\.]*)`)
  1178  		//nameserverRegex.ReplaceAllFunc([]byte(etcResolv), []byte(`$1`))
  1179  		res := nameserverRegex.FindStringSubmatch(etcResolv)
  1180  		if res == nil || len(res) != 2 {
  1181  			util.Warning("unable to determine host.docker.internal from /etc/resolv.conf")
  1182  			return ""
  1183  		}
  1184  		return res[1]
  1185  	}
  1186  	util.Warning("inappropriately using wsl2ResolvConfNameserver() but not on WSL2")
  1187  	return ""
  1188  }
  1189  
  1190  // RemoveImage removes an image with force
  1191  func RemoveImage(tag string) error {
  1192  	client := GetDockerClient()
  1193  	_, err := client.InspectImage(tag)
  1194  	if err == nil {
  1195  		err = client.RemoveImageExtended(tag, docker.RemoveImageOptions{Force: true})
  1196  
  1197  		if err == nil {
  1198  			util.Debug("Deleted docker image %s", tag)
  1199  		} else {
  1200  			util.Warning("Unable to delete %s: %v", tag, err)
  1201  		}
  1202  	}
  1203  	return nil
  1204  }
  1205  
  1206  // CopyIntoVolume copies a file or directory on the host into a docker volume
  1207  // sourcePath is the host-side full path
  1208  // volumeName is the volume name to copy to
  1209  // targetSubdir is where to copy it to on the volume
  1210  // uid is the uid of the resulting files
  1211  // exclusion is a path to be excluded
  1212  // If destroyExisting the volume is removed and recreated
  1213  func CopyIntoVolume(sourcePath string, volumeName string, targetSubdir string, uid string, exclusion string, destroyExisting bool) error {
  1214  	if destroyExisting {
  1215  		err := RemoveVolume(volumeName)
  1216  		if err != nil {
  1217  			util.Warning("could not remove docker volume %s: %v", volumeName, err)
  1218  		}
  1219  	}
  1220  	volPath := "/mnt/v"
  1221  	targetSubdirFullPath := volPath + "/" + targetSubdir
  1222  	_, err := os.Stat(sourcePath)
  1223  	if err != nil {
  1224  		return err
  1225  	}
  1226  
  1227  	f, err := os.Open(sourcePath)
  1228  	if err != nil {
  1229  		util.Failed("Failed to open %s: %v", sourcePath, err)
  1230  	}
  1231  
  1232  	// nolint errcheck
  1233  	defer f.Close()
  1234  
  1235  	containerName := "CopyIntoVolume_" + nodeps.RandomString(12)
  1236  
  1237  	track := util.TimeTrack(time.Now(), "CopyIntoVolume "+sourcePath+" "+volumeName)
  1238  	containerID, _, err := RunSimpleContainer(versionconstants.GetWebImage(), containerName, []string{"sh", "-c", "mkdir -p " + targetSubdirFullPath + " && tail -f /dev/null"}, nil, nil, []string{volumeName + ":" + volPath}, "0", false, true, nil)
  1239  	if err != nil {
  1240  		return err
  1241  	}
  1242  	// nolint: errcheck
  1243  	defer RemoveContainer(containerID, 0)
  1244  
  1245  	err = CopyIntoContainer(sourcePath, containerName, targetSubdirFullPath, exclusion)
  1246  
  1247  	if err != nil {
  1248  		return err
  1249  	}
  1250  
  1251  	// chown/chmod the uploaded content
  1252  	c := fmt.Sprintf("chown -R %s %s", uid, targetSubdirFullPath)
  1253  	stdout, stderr, err := Exec(containerID, c, "0")
  1254  	util.Debug("Exec %s stdout=%s, stderr=%s, err=%v", c, stdout, stderr, err)
  1255  
  1256  	if err != nil {
  1257  		return err
  1258  	}
  1259  	track()
  1260  	return nil
  1261  }
  1262  
  1263  // Exec does a simple docker exec, no frills, just executes the command
  1264  // with the specified uid (or defaults to root=0 if empty uid)
  1265  // Returns stdout, stderr, error
  1266  func Exec(containerID string, command string, uid string) (string, string, error) {
  1267  	client := GetDockerClient()
  1268  
  1269  	if uid == "" {
  1270  		uid = "0"
  1271  	}
  1272  	exec, err := client.CreateExec(docker.CreateExecOptions{
  1273  		Container:    containerID,
  1274  		Cmd:          []string{"sh", "-c", command},
  1275  		AttachStdout: true,
  1276  		AttachStderr: true,
  1277  		User:         uid,
  1278  	})
  1279  	if err != nil {
  1280  		return "", "", err
  1281  	}
  1282  
  1283  	var stdout, stderr bytes.Buffer
  1284  	err = client.StartExec(exec.ID, docker.StartExecOptions{
  1285  		OutputStream: &stdout,
  1286  		ErrorStream:  &stderr,
  1287  		Detach:       false,
  1288  	})
  1289  	if err != nil {
  1290  		return "", "", err
  1291  	}
  1292  
  1293  	info, err := client.InspectExec(exec.ID)
  1294  	if err != nil {
  1295  		return stdout.String(), stderr.String(), err
  1296  	}
  1297  	var execErr error
  1298  	if info.ExitCode != 0 {
  1299  		execErr = fmt.Errorf("command '%s' returned exit code %v", command, info.ExitCode)
  1300  	}
  1301  
  1302  	return stdout.String(), stderr.String(), execErr
  1303  }
  1304  
  1305  // CheckAvailableSpace outputs a warning if docker space is low
  1306  func CheckAvailableSpace() {
  1307  	_, out, _ := RunSimpleContainer(versionconstants.GetWebImage(), "", []string{"sh", "-c", `df / | awk '!/Mounted/ {print $4, $5;}'`}, []string{}, []string{}, []string{}, "", true, false, nil)
  1308  	out = strings.Trim(out, "% \r\n")
  1309  	parts := strings.Split(out, " ")
  1310  	if len(parts) != 2 {
  1311  		util.Warning("Unable to determine docker space usage: %s", out)
  1312  		return
  1313  	}
  1314  	spacePercent, _ := strconv.Atoi(parts[1])
  1315  	spaceAbsolute, _ := strconv.Atoi(parts[0]) // Note that this is in KB
  1316  
  1317  	if spaceAbsolute < nodeps.MinimumDockerSpaceWarning {
  1318  		util.Error("Your docker install has only %d available disk space, less than %d warning level (%d%% used). Please increase disk image size.", spaceAbsolute, nodeps.MinimumDockerSpaceWarning, spacePercent)
  1319  	}
  1320  }
  1321  
  1322  // DownloadDockerComposeIfNeeded downloads the proper version of docker-compose
  1323  // if it's either not yet installed or has the wrong version.
  1324  // Returns downloaded bool (true if it did the download) and err
  1325  func DownloadDockerComposeIfNeeded() (bool, error) {
  1326  	requiredVersion := globalconfig.GetRequiredDockerComposeVersion()
  1327  	var err error
  1328  	if requiredVersion == "" {
  1329  		util.Debug("globalconfig use_docker_compose_from_path is set, so not downloading")
  1330  		return false, nil
  1331  	}
  1332  	curVersion, err := GetLiveDockerComposeVersion()
  1333  	if err != nil || curVersion != requiredVersion {
  1334  		err = DownloadDockerCompose()
  1335  		if err == nil {
  1336  			return true, err
  1337  		}
  1338  	}
  1339  	return false, err
  1340  }
  1341  
  1342  // DownloadDockerCompose gets the docker-compose binary and puts it into
  1343  // ~/.ddev/.bin
  1344  func DownloadDockerCompose() error {
  1345  	globalBinDir := globalconfig.GetDDEVBinDir()
  1346  	destFile, _ := globalconfig.GetDockerComposePath()
  1347  
  1348  	composeURL, err := dockerComposeDownloadLink()
  1349  	if err != nil {
  1350  		return err
  1351  	}
  1352  	output.UserOut.Printf("Downloading %s ...", composeURL)
  1353  
  1354  	_ = os.Remove(destFile)
  1355  
  1356  	_ = os.MkdirAll(globalBinDir, 0777)
  1357  	err = util.DownloadFile(destFile, composeURL, "true" != os.Getenv("DDEV_NONINTERACTIVE"))
  1358  	if err != nil {
  1359  		return err
  1360  	}
  1361  	output.UserOut.Printf("Download complete.")
  1362  
  1363  	// Remove the cached DockerComposeVersion
  1364  	globalconfig.DockerComposeVersion = ""
  1365  
  1366  	err = os.Chmod(destFile, 0755)
  1367  	if err != nil {
  1368  		return err
  1369  	}
  1370  
  1371  	return nil
  1372  }
  1373  
  1374  func dockerComposeDownloadLink() (string, error) {
  1375  	v := globalconfig.GetRequiredDockerComposeVersion()
  1376  	if len(v) < 3 {
  1377  		return "", fmt.Errorf("required docker-compose version is invalid: %v", v)
  1378  	}
  1379  	baseVersion := v[1:2]
  1380  
  1381  	switch baseVersion {
  1382  	case "2":
  1383  		return dockerComposeDownloadLinkV2()
  1384  	}
  1385  	return "", fmt.Errorf("Invalid docker-compose base version %s", v)
  1386  }
  1387  
  1388  // dockerComposeDownloadLinkV2 downlods compose v1 downloads like
  1389  //   https://github.com/docker/compose/releases/download/v2.2.1/docker-compose-darwin-aarch64
  1390  //   https://github.com/docker/compose/releases/download/v2.2.1/docker-compose-darwin-x86_64
  1391  //   https://github.com/docker/compose/releases/download/v2.2.1/docker-compose-windows-x86_64.exe
  1392  
  1393  func dockerComposeDownloadLinkV2() (string, error) {
  1394  	arch := runtime.GOARCH
  1395  
  1396  	switch arch {
  1397  	case "arm64":
  1398  		arch = "aarch64"
  1399  	case "amd64":
  1400  		arch = "x86_64"
  1401  	default:
  1402  		return "", fmt.Errorf("Only arm64 and amd64 architectures are supported for docker-compose v2, not %s", arch)
  1403  	}
  1404  	flavor := runtime.GOOS + "-" + arch
  1405  	ComposeURL := fmt.Sprintf("https://github.com/docker/compose/releases/download/%s/docker-compose-%s", globalconfig.GetRequiredDockerComposeVersion(), flavor)
  1406  	if runtime.GOOS == "windows" {
  1407  		ComposeURL = ComposeURL + ".exe"
  1408  	}
  1409  	return ComposeURL, nil
  1410  }
  1411  
  1412  // IsDockerDesktop detects if running on Docker Desktop
  1413  func IsDockerDesktop() bool {
  1414  	client := GetDockerClient()
  1415  	info, err := client.Info()
  1416  	if err != nil {
  1417  		util.Warning("IsDockerDesktop(): Unable to get docker info, err=%v", err)
  1418  		return false
  1419  	}
  1420  	if info.OperatingSystem == "Docker Desktop" {
  1421  		return true
  1422  	}
  1423  	return false
  1424  }
  1425  
  1426  // IsColima detects if running on Colima
  1427  func IsColima() bool {
  1428  	client := GetDockerClient()
  1429  	info, err := client.Info()
  1430  	if err != nil {
  1431  		util.Warning("IsColima(): Unable to get docker info, err=%v", err)
  1432  		return false
  1433  	}
  1434  	if strings.HasPrefix(info.Name, "colima") {
  1435  		return true
  1436  	}
  1437  	return false
  1438  }
  1439  
  1440  // CopyIntoContainer copies a path (file or directory) into a specified container and location
  1441  func CopyIntoContainer(srcPath string, containerName string, dstPath string, exclusion string) error {
  1442  	startTime := time.Now()
  1443  	fi, err := os.Stat(srcPath)
  1444  	if err != nil {
  1445  		return err
  1446  	}
  1447  	// If a file has been passed in, we'll copy it into a temp directory
  1448  	if !fi.IsDir() {
  1449  		dirName, err := os.MkdirTemp("", "")
  1450  		if err != nil {
  1451  			return err
  1452  		}
  1453  		defer os.RemoveAll(dirName)
  1454  		err = fileutil.CopyFile(srcPath, filepath.Join(dirName, filepath.Base(srcPath)))
  1455  		if err != nil {
  1456  			return err
  1457  		}
  1458  		srcPath = dirName
  1459  	}
  1460  
  1461  	client := GetDockerClient()
  1462  	cid, err := FindContainerByName(containerName)
  1463  	if err != nil {
  1464  		return err
  1465  	}
  1466  	if cid == nil {
  1467  		return fmt.Errorf("CopyIntoContainer unable to find a container named %s", containerName)
  1468  	}
  1469  
  1470  	uid, _, _ := util.GetContainerUIDGid()
  1471  	_, stderr, err := Exec(cid.ID, "mkdir -p "+dstPath, uid)
  1472  	if err != nil {
  1473  		return fmt.Errorf("unable to mkdir -p %s inside %s: %v (stderr=%s)", dstPath, containerName, err, stderr)
  1474  	}
  1475  
  1476  	tarball, err := os.CreateTemp(os.TempDir(), "containercopytmp*.tar.gz")
  1477  	if err != nil {
  1478  		return err
  1479  	}
  1480  	err = tarball.Close()
  1481  	if err != nil {
  1482  		return err
  1483  	}
  1484  	// nolint: errcheck
  1485  	defer os.Remove(tarball.Name())
  1486  
  1487  	// Tar up the source directory into the tarball
  1488  	err = archive.Tar(srcPath, tarball.Name(), exclusion)
  1489  	if err != nil {
  1490  		return err
  1491  	}
  1492  	t, err := os.Open(tarball.Name())
  1493  	if err != nil {
  1494  		return err
  1495  	}
  1496  
  1497  	// nolint: errcheck
  1498  	defer t.Close()
  1499  
  1500  	err = client.UploadToContainer(cid.ID, docker.UploadToContainerOptions{
  1501  		InputStream: t,
  1502  		Path:        dstPath,
  1503  	})
  1504  	if err != nil {
  1505  		return err
  1506  	}
  1507  
  1508  	util.Debug("Copied %s:%s into %s in %v", srcPath, containerName, dstPath, time.Since(startTime))
  1509  	return nil
  1510  }
  1511  
  1512  // CopyFromContainer copies a path from a specified container and location to a dstPath on host
  1513  func CopyFromContainer(containerName string, containerPath string, hostPath string) error {
  1514  	startTime := time.Now()
  1515  	err := os.MkdirAll(hostPath, 0755)
  1516  	if err != nil {
  1517  		return err
  1518  	}
  1519  
  1520  	client := GetDockerClient()
  1521  	cid, err := FindContainerByName(containerName)
  1522  	if err != nil {
  1523  		return err
  1524  	}
  1525  	if cid == nil {
  1526  		return fmt.Errorf("CopyFromContainer unable to find a container named %s", containerName)
  1527  	}
  1528  
  1529  	f, err := os.CreateTemp("", filepath.Base(hostPath)+".tar.gz")
  1530  	if err != nil {
  1531  		return err
  1532  	}
  1533  	//nolint: errcheck
  1534  	defer f.Close()
  1535  	//nolint: errcheck
  1536  	defer os.Remove(f.Name())
  1537  	// nolint: errcheck
  1538  
  1539  	err = client.DownloadFromContainer(cid.ID, docker.DownloadFromContainerOptions{
  1540  		Path:         containerPath,
  1541  		OutputStream: f,
  1542  	})
  1543  	if err != nil {
  1544  		return err
  1545  	}
  1546  
  1547  	err = f.Close()
  1548  	if err != nil {
  1549  		return err
  1550  	}
  1551  
  1552  	err = archive.Untar(f.Name(), hostPath, "")
  1553  	if err != nil {
  1554  		return err
  1555  	}
  1556  	util.Success("Copied %s:%s to %s in %v", containerName, containerPath, hostPath, time.Since(startTime))
  1557  
  1558  	return nil
  1559  }
  1560  
  1561  // DockerVersionConstraint is the current minimum version of docker required for ddev.
  1562  // See https://godoc.org/github.com/Masterminds/semver#hdr-Checking_Version_Constraints
  1563  // for examples defining version constraints.
  1564  // REMEMBER TO CHANGE docs/ddev-installation.md if you touch this!
  1565  // The constraint MUST HAVE a -pre of some kind on it for successful comparison.
  1566  // See https://github.com/drud/ddev/pull/738.. and regression https://github.com/drud/ddev/issues/1431
  1567  var DockerVersionConstraint = ">= 20.10.0-alpha1"
  1568  
  1569  // DockerVersion is cached version of docker
  1570  var DockerVersion = ""
  1571  
  1572  // GetDockerVersion gets the cached or api-sourced version of docker engine
  1573  func GetDockerVersion() (string, error) {
  1574  	if DockerVersion != "" {
  1575  		return DockerVersion, nil
  1576  	}
  1577  	client := GetDockerClient()
  1578  	if client == nil {
  1579  		return "", fmt.Errorf("Unable to get docker version: docker client is nil")
  1580  	}
  1581  
  1582  	v, err := client.Version()
  1583  	if err != nil {
  1584  		return "", err
  1585  	}
  1586  	DockerVersion = v.Get("Version")
  1587  
  1588  	return DockerVersion, nil
  1589  }
  1590  
  1591  // DockerComposeVersionConstraint is the versions allowed for ddev
  1592  // REMEMBER TO CHANGE docs/ddev-installation.md if you touch this!
  1593  // The constraint MUST HAVE a -pre of some kind on it for successful comparison.
  1594  // See https://github.com/drud/ddev/pull/738.. and regression https://github.com/drud/ddev/issues/1431
  1595  var DockerComposeVersionConstraint = ">= 2.5.1"
  1596  
  1597  // GetDockerComposeVersion runs docker-compose -v to get the current version
  1598  func GetDockerComposeVersion() (string, error) {
  1599  	if globalconfig.DockerComposeVersion != "" {
  1600  		return globalconfig.DockerComposeVersion, nil
  1601  	}
  1602  
  1603  	return GetLiveDockerComposeVersion()
  1604  }
  1605  
  1606  // GetLiveDockerComposeVersion runs `docker-compose --version` and caches result
  1607  func GetLiveDockerComposeVersion() (string, error) {
  1608  	if globalconfig.DockerComposeVersion != "" {
  1609  		return globalconfig.DockerComposeVersion, nil
  1610  	}
  1611  
  1612  	composePath, err := globalconfig.GetDockerComposePath()
  1613  	if err != nil {
  1614  		return "", err
  1615  	}
  1616  
  1617  	if !fileutil.FileExists(composePath) {
  1618  		globalconfig.DockerComposeVersion = ""
  1619  		return globalconfig.DockerComposeVersion, fmt.Errorf("docker-compose does not exist at %s", composePath)
  1620  	}
  1621  	out, err := exec.Command(composePath, "version", "--short").Output()
  1622  	if err != nil {
  1623  		return "", err
  1624  	}
  1625  	v := strings.Trim(string(out), "\r\n")
  1626  
  1627  	// docker-compose v1 and v2.3.3 return a version without the prefix "v", so add it.
  1628  	if !strings.HasPrefix(v, "v") {
  1629  		v = "v" + v
  1630  	}
  1631  
  1632  	globalconfig.DockerComposeVersion = v
  1633  	return globalconfig.DockerComposeVersion, nil
  1634  }