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

     1  package ddevapp
     2  
     3  import (
     4  	"bytes"
     5  	"embed"
     6  	"fmt"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"runtime"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/drud/ddev/pkg/appimport"
    16  	"github.com/drud/ddev/pkg/archive"
    17  	"github.com/drud/ddev/pkg/dockerutil"
    18  	"github.com/drud/ddev/pkg/exec"
    19  	"github.com/drud/ddev/pkg/fileutil"
    20  	"github.com/drud/ddev/pkg/globalconfig"
    21  	"github.com/drud/ddev/pkg/nodeps"
    22  	"github.com/drud/ddev/pkg/output"
    23  	"github.com/drud/ddev/pkg/util"
    24  	"github.com/drud/ddev/pkg/versionconstants"
    25  	docker "github.com/fsouza/go-dockerclient"
    26  	"github.com/mattn/go-isatty"
    27  	"github.com/otiai10/copy"
    28  	"golang.org/x/term"
    29  	"gopkg.in/yaml.v3"
    30  )
    31  
    32  // SiteRunning defines the string used to denote running sites.
    33  const SiteRunning = "running"
    34  
    35  // SiteStarting is the string for a project that is starting
    36  const SiteStarting = "starting"
    37  
    38  // SiteStopped defines the string used to denote a site where the containers were not found/do not exist, but the project is there.
    39  const SiteStopped = "stopped"
    40  
    41  // SiteDirMissing defines the string used to denote when a site is missing its application directory.
    42  const SiteDirMissing = "project directory missing"
    43  
    44  // SiteConfigMissing defines the string used to denote when a site is missing its .ddev/config.yml file.
    45  const SiteConfigMissing = ".ddev/config.yaml missing"
    46  
    47  // SitePaused defines the string used to denote when a site is in the paused (docker stopped) state.
    48  const SitePaused = "paused"
    49  
    50  // SiteUnhealthy is the status for a project whose services are not all running
    51  const SiteUnhealthy = "unhealthy"
    52  
    53  // DatabaseDefault is the default database/version
    54  var DatabaseDefault = DatabaseDesc{nodeps.MariaDB, nodeps.MariaDBDefaultVersion}
    55  
    56  type DatabaseDesc struct {
    57  	Type    string `yaml:"type"`
    58  	Version string `yaml:"version"`
    59  }
    60  
    61  type WebExposedPort struct {
    62  	Name             string `yaml:"name"`
    63  	WebContainerPort int    `yaml:"container_port"`
    64  	HTTPPort         int    `yaml:"http_port"`
    65  	HTTPSPort        int    `yaml:"https_port"`
    66  }
    67  
    68  type WebExtraDaemon struct {
    69  	Name      string `yaml:"name"`
    70  	Command   string `yaml:"command"`
    71  	Directory string `yaml:"directory"`
    72  }
    73  
    74  // DdevApp is the struct that represents a ddev app, mostly its config
    75  // from config.yaml.
    76  type DdevApp struct {
    77  	Name                  string                `yaml:"name"`
    78  	Type                  string                `yaml:"type"`
    79  	Docroot               string                `yaml:"docroot"`
    80  	PHPVersion            string                `yaml:"php_version"`
    81  	WebserverType         string                `yaml:"webserver_type"`
    82  	WebImage              string                `yaml:"webimage,omitempty"`
    83  	DBImage               string                `yaml:"dbimage,omitempty"`
    84  	DBAImage              string                `yaml:"dbaimage,omitempty"`
    85  	RouterHTTPPort        string                `yaml:"router_http_port"`
    86  	RouterHTTPSPort       string                `yaml:"router_https_port"`
    87  	XdebugEnabled         bool                  `yaml:"xdebug_enabled"`
    88  	NoProjectMount        bool                  `yaml:"no_project_mount,omitempty"`
    89  	AdditionalHostnames   []string              `yaml:"additional_hostnames"`
    90  	AdditionalFQDNs       []string              `yaml:"additional_fqdns"`
    91  	MariaDBVersion        string                `yaml:"mariadb_version,omitempty"`
    92  	MySQLVersion          string                `yaml:"mysql_version,omitempty"`
    93  	Database              DatabaseDesc          `yaml:"database"`
    94  	NFSMountEnabled       bool                  `yaml:"nfs_mount_enabled"`
    95  	NFSMountEnabledGlobal bool                  `yaml:"-"`
    96  	MutagenEnabled        bool                  `yaml:"mutagen_enabled"`
    97  	MutagenEnabledGlobal  bool                  `yaml:"-"`
    98  	FailOnHookFail        bool                  `yaml:"fail_on_hook_fail,omitempty"`
    99  	BindAllInterfaces     bool                  `yaml:"bind_all_interfaces,omitempty"`
   100  	FailOnHookFailGlobal  bool                  `yaml:"-"`
   101  	ConfigPath            string                `yaml:"-"`
   102  	AppRoot               string                `yaml:"-"`
   103  	DataDir               string                `yaml:"-"`
   104  	SiteSettingsPath      string                `yaml:"-"`
   105  	SiteDdevSettingsFile  string                `yaml:"-"`
   106  	ProviderInstance      *Provider             `yaml:"-"`
   107  	Hooks                 map[string][]YAMLTask `yaml:"hooks,omitempty"`
   108  	UploadDir             string                `yaml:"upload_dir,omitempty"`
   109  	WorkingDir            map[string]string     `yaml:"working_dir,omitempty"`
   110  	OmitContainers        []string              `yaml:"omit_containers,omitempty,flow"`
   111  	OmitContainersGlobal  []string              `yaml:"-"`
   112  	HostDBPort            string                `yaml:"host_db_port,omitempty"`
   113  	HostWebserverPort     string                `yaml:"host_webserver_port,omitempty"`
   114  	HostHTTPSPort         string                `yaml:"host_https_port,omitempty"`
   115  	MailhogPort           string                `yaml:"mailhog_port,omitempty"`
   116  	MailhogHTTPSPort      string                `yaml:"mailhog_https_port,omitempty"`
   117  	HostMailhogPort       string                `yaml:"host_mailhog_port,omitempty"`
   118  	PHPMyAdminPort        string                `yaml:"phpmyadmin_port,omitempty"`
   119  	PHPMyAdminHTTPSPort   string                `yaml:"phpmyadmin_https_port,omitempty"`
   120  	// HostPHPMyAdminPort is normally empty, as it is not normally bound
   121  	HostPHPMyAdminPort        string                 `yaml:"host_phpmyadmin_port,omitempty"`
   122  	WebImageExtraPackages     []string               `yaml:"webimage_extra_packages,omitempty,flow"`
   123  	DBImageExtraPackages      []string               `yaml:"dbimage_extra_packages,omitempty,flow"`
   124  	ProjectTLD                string                 `yaml:"project_tld,omitempty"`
   125  	UseDNSWhenPossible        bool                   `yaml:"use_dns_when_possible"`
   126  	MkcertEnabled             bool                   `yaml:"-"`
   127  	NgrokArgs                 string                 `yaml:"ngrok_args,omitempty"`
   128  	Timezone                  string                 `yaml:"timezone,omitempty"`
   129  	ComposerRoot              string                 `yaml:"composer_root,omitempty"`
   130  	ComposerVersion           string                 `yaml:"composer_version"`
   131  	DisableSettingsManagement bool                   `yaml:"disable_settings_management,omitempty"`
   132  	WebEnvironment            []string               `yaml:"web_environment"`
   133  	NodeJSVersion             string                 `yaml:"nodejs_version"`
   134  	DefaultContainerTimeout   string                 `yaml:"default_container_timeout,omitempty"`
   135  	WebExtraExposedPorts      []WebExposedPort       `yaml:"web_extra_exposed_ports,omitempty"`
   136  	WebExtraDaemons           []WebExtraDaemon       `yaml:"web_extra_daemons,omitempty"`
   137  	OverrideConfig            bool                   `yaml:"override_config,omitempty"`
   138  	ComposeYaml               map[string]interface{} `yaml:"-"`
   139  }
   140  
   141  // GetType returns the application type as a (lowercase) string
   142  func (app *DdevApp) GetType() string {
   143  	return strings.ToLower(app.Type)
   144  }
   145  
   146  // Init populates DdevApp config based on the current working directory.
   147  // It does not start the containers.
   148  func (app *DdevApp) Init(basePath string) error {
   149  	runTime := util.TimeTrack(time.Now(), fmt.Sprintf("app.Init(%s)", basePath))
   150  	defer runTime()
   151  
   152  	newApp, err := NewApp(basePath, true)
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	err = newApp.ValidateConfig()
   158  	if err != nil {
   159  		return err
   160  	}
   161  
   162  	*app = *newApp
   163  	web, err := app.FindContainerByType("web")
   164  
   165  	if err != nil {
   166  		return err
   167  	}
   168  
   169  	if web != nil {
   170  		containerApproot := web.Labels["com.ddev.approot"]
   171  		isSameFile, err := fileutil.IsSameFile(containerApproot, app.AppRoot)
   172  		if err != nil {
   173  			return err
   174  		}
   175  		if !isSameFile {
   176  			return fmt.Errorf("a project (web container) in %s state already exists for %s that was created at %s", web.State, app.Name, containerApproot).(webContainerExists)
   177  		}
   178  		return nil
   179  	}
   180  	// Init() is just putting together the DdevApp struct, the containers do
   181  	// not have to exist (app doesn't have to have been started), so the fact
   182  	// we didn't find any is not an error.
   183  	return nil
   184  }
   185  
   186  // FindContainerByType will find a container for this site denoted by the containerType if it is available.
   187  func (app *DdevApp) FindContainerByType(containerType string) (*docker.APIContainers, error) {
   188  	labels := map[string]string{
   189  		"com.ddev.site-name":         app.GetName(),
   190  		"com.docker.compose.service": containerType,
   191  	}
   192  
   193  	return dockerutil.FindContainerByLabels(labels)
   194  }
   195  
   196  // Describe returns a map which provides detailed information on services associated with the running site.
   197  func (app *DdevApp) Describe(short bool) (map[string]interface{}, error) {
   198  	app.DockerEnv()
   199  	err := app.ProcessHooks("pre-describe")
   200  	if err != nil {
   201  		return nil, fmt.Errorf("failed to process pre-describe hooks: %v", err)
   202  	}
   203  
   204  	shortRoot := RenderHomeRootedDir(app.GetAppRoot())
   205  	appDesc := make(map[string]interface{})
   206  	status, statusDesc := app.SiteStatus()
   207  
   208  	appDesc["name"] = app.GetName()
   209  	appDesc["status"] = status
   210  	appDesc["status_desc"] = statusDesc
   211  	appDesc["approot"] = app.GetAppRoot()
   212  	appDesc["docroot"] = app.GetDocroot()
   213  	appDesc["shortroot"] = shortRoot
   214  	appDesc["httpurl"] = app.GetHTTPURL()
   215  	appDesc["httpsurl"] = app.GetHTTPSURL()
   216  	appDesc["router_disabled"] = IsRouterDisabled(app)
   217  	appDesc["primary_url"] = app.GetPrimaryURL()
   218  	appDesc["type"] = app.GetType()
   219  	appDesc["mutagen_enabled"] = app.IsMutagenEnabled()
   220  	appDesc["nodejs_version"] = app.NodeJSVersion
   221  	appDesc["use_traefik"] = globalconfig.DdevGlobalConfig.UseTraefik
   222  	if app.IsMutagenEnabled() {
   223  		appDesc["mutagen_status"], _, _, err = app.MutagenStatus()
   224  		if err != nil {
   225  			appDesc["mutagen_status"] = err.Error() + " " + appDesc["mutagen_status"].(string)
   226  		}
   227  	}
   228  
   229  	// if short is set, we don't need more information, so return what we have.
   230  	if short {
   231  		return appDesc, nil
   232  	}
   233  	appDesc["hostname"] = app.GetHostname()
   234  	appDesc["hostnames"] = app.GetHostnames()
   235  	appDesc["nfs_mount_enabled"] = app.IsNFSMountEnabled()
   236  	appDesc["fail_on_hook_fail"] = app.FailOnHookFail || app.FailOnHookFailGlobal
   237  	httpURLs, httpsURLs, allURLs := app.GetAllURLs()
   238  	appDesc["httpURLs"] = httpURLs
   239  	appDesc["httpsURLs"] = httpsURLs
   240  	appDesc["urls"] = allURLs
   241  
   242  	appDesc["database_type"] = app.Database.Type
   243  	appDesc["database_version"] = app.Database.Version
   244  
   245  	// Only show extended status for running sites.
   246  	if status == SiteRunning {
   247  		if !nodeps.ArrayContainsString(app.GetOmittedContainers(), "db") {
   248  			dbinfo := make(map[string]interface{})
   249  			dbinfo["username"] = "db"
   250  			dbinfo["password"] = "db"
   251  			dbinfo["dbname"] = "db"
   252  			dbinfo["host"] = "db"
   253  			dbPublicPort, err := app.GetPublishedPort("db")
   254  			util.CheckErr(err)
   255  			dbinfo["dbPort"] = GetExposedPort(app, "db")
   256  			util.CheckErr(err)
   257  			dbinfo["published_port"] = dbPublicPort
   258  			dbinfo["database_type"] = "mariadb" // default
   259  			dbinfo["database_type"] = app.Database.Type
   260  			dbinfo["database_version"] = app.Database.Version
   261  
   262  			appDesc["dbinfo"] = dbinfo
   263  
   264  			if !nodeps.ArrayContainsString(app.GetOmittedContainers(), "dba") {
   265  				appDesc["phpmyadmin_https_url"] = "https://" + app.GetHostname() + ":" + app.PHPMyAdminHTTPSPort
   266  				appDesc["phpmyadmin_url"] = "http://" + app.GetHostname() + ":" + app.PHPMyAdminPort
   267  			}
   268  		}
   269  
   270  		appDesc["mailhog_https_url"] = "https://" + app.GetHostname() + ":" + app.MailhogHTTPSPort
   271  		appDesc["mailhog_url"] = "http://" + app.GetHostname() + ":" + app.MailhogPort
   272  	}
   273  
   274  	routerStatus, logOutput := GetRouterStatus()
   275  	appDesc["router_status"] = routerStatus
   276  	appDesc["router_status_log"] = logOutput
   277  	appDesc["ssh_agent_status"] = GetSSHAuthStatus()
   278  	appDesc["php_version"] = app.GetPhpVersion()
   279  	appDesc["webserver_type"] = app.GetWebserverType()
   280  
   281  	appDesc["router_http_port"] = app.RouterHTTPPort
   282  	appDesc["router_https_port"] = app.RouterHTTPSPort
   283  	appDesc["xdebug_enabled"] = app.XdebugEnabled
   284  	appDesc["webimg"] = app.WebImage
   285  	appDesc["dbimg"] = app.GetDBImage()
   286  	appDesc["services"] = map[string]map[string]string{}
   287  
   288  	containers, err := dockerutil.GetAppContainers(app.Name)
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  	services := appDesc["services"].(map[string]map[string]string)
   293  	for _, k := range containers {
   294  		serviceName := strings.TrimPrefix(k.Names[0], "/")
   295  		shortName := strings.Replace(serviceName, fmt.Sprintf("ddev-%s-", app.Name), "", 1)
   296  
   297  		c, err := dockerutil.InspectContainer(serviceName)
   298  		if err != nil || c == nil {
   299  			util.Warning("Could not get container info for %s", serviceName)
   300  			continue
   301  		}
   302  		fullName := strings.TrimPrefix(serviceName, "/")
   303  		services[shortName] = map[string]string{}
   304  		services[shortName]["status"] = c.State.Status
   305  		services[shortName]["full_name"] = fullName
   306  		services[shortName]["image"] = strings.TrimSuffix(c.Config.Image, fmt.Sprintf("-%s-built", app.Name))
   307  		services[shortName]["short_name"] = shortName
   308  		var ports []string
   309  		for pk := range c.Config.ExposedPorts {
   310  			ports = append(ports, pk.Port())
   311  		}
   312  		services[shortName]["exposed_ports"] = strings.Join(ports, ",")
   313  		var hostPorts []string
   314  		for _, pv := range k.Ports {
   315  			if pv.PublicPort != 0 {
   316  				hostPorts = append(hostPorts, strconv.FormatInt(pv.PublicPort, 10))
   317  			}
   318  		}
   319  		services[shortName]["host_ports"] = strings.Join(hostPorts, ",")
   320  
   321  		// Extract HTTP_EXPOSE and HTTPS_EXPOSE for additional info
   322  		if !IsRouterDisabled(app) {
   323  			for _, e := range c.Config.Env {
   324  				split := strings.SplitN(e, "=", 2)
   325  				envName := split[0]
   326  				if len(split) == 2 && (envName == "HTTP_EXPOSE" || envName == "HTTPS_EXPOSE") {
   327  					envVal := split[1]
   328  
   329  					envValStr := fmt.Sprintf("%s", envVal)
   330  					portSpecs := strings.Split(envValStr, ",")
   331  					// There might be more than one exposed UI port, but this only handles the first listed,
   332  					// most often there's only one.
   333  					if len(portSpecs) > 0 {
   334  						// HTTPS portSpecs typically look like <exposed>:<containerPort>, for example - HTTPS_EXPOSE=1359:1358
   335  						ports := strings.Split(portSpecs[0], ":")
   336  						//services[shortName][envName.(string)] = ports[0]
   337  						switch envName {
   338  						case "HTTP_EXPOSE":
   339  							services[shortName]["http_url"] = "http://" + appDesc["hostname"].(string)
   340  							if ports[0] != "80" {
   341  								services[shortName]["http_url"] = services[shortName]["http_url"] + ":" + ports[0]
   342  							}
   343  						case "HTTPS_EXPOSE":
   344  							services[shortName]["https_url"] = "https://" + appDesc["hostname"].(string)
   345  							if ports[0] != "443" {
   346  								services[shortName]["https_url"] = services[shortName]["https_url"] + ":" + ports[0]
   347  							}
   348  						}
   349  					}
   350  				}
   351  			}
   352  		}
   353  		if shortName == "web" {
   354  			services[shortName]["host_http_url"] = app.GetWebContainerDirectHTTPURL()
   355  			services[shortName]["host_https_url"] = app.GetWebContainerDirectHTTPSURL()
   356  		}
   357  	}
   358  
   359  	err = app.ProcessHooks("post-describe")
   360  	if err != nil {
   361  		return nil, fmt.Errorf("failed to process post-describe hooks: %v", err)
   362  	}
   363  
   364  	return appDesc, nil
   365  }
   366  
   367  // GetPublishedPort returns the host-exposed public port of a container.
   368  func (app *DdevApp) GetPublishedPort(serviceName string) (int, error) {
   369  	container, err := app.FindContainerByType(serviceName)
   370  	if err != nil || container == nil {
   371  		return -1, fmt.Errorf("failed to find container of type %s: %v", serviceName, err)
   372  	}
   373  
   374  	privatePort, _ := strconv.ParseInt(GetExposedPort(app, serviceName), 10, 16)
   375  
   376  	publishedPort := dockerutil.GetPublishedPort(privatePort, *container)
   377  	return publishedPort, nil
   378  }
   379  
   380  // GetOmittedContainers returns full list of global and local omitted containers
   381  func (app *DdevApp) GetOmittedContainers() []string {
   382  	omitted := app.OmitContainersGlobal
   383  	omitted = append(omitted, app.OmitContainers...)
   384  	return omitted
   385  }
   386  
   387  // GetAppRoot return the full path from root to the app directory
   388  func (app *DdevApp) GetAppRoot() string {
   389  	return app.AppRoot
   390  }
   391  
   392  // AppConfDir returns the full path to the app's .ddev configuration directory
   393  func (app *DdevApp) AppConfDir() string {
   394  	return filepath.Join(app.AppRoot, ".ddev")
   395  }
   396  
   397  // GetDocroot returns the docroot path for ddev app
   398  func (app DdevApp) GetDocroot() string {
   399  	return app.Docroot
   400  }
   401  
   402  // GetComposerRoot will determine the absolute composer root directory where
   403  // all Composer related commands will be executed.
   404  // If inContainer set to true, the absolute path in the container will be
   405  // returned, else the absolute path on the host.
   406  // If showWarning set to true, a warning containing the composer root will be
   407  // shown to the user to avoid confusion.
   408  func (app *DdevApp) GetComposerRoot(inContainer, showWarning bool) string {
   409  	basePath := ""
   410  
   411  	if inContainer {
   412  		basePath = "/var/www/html"
   413  	} else {
   414  		basePath = app.AppRoot
   415  	}
   416  
   417  	absComposerRoot := path.Join(basePath, app.ComposerRoot)
   418  
   419  	// If requested, let the user know we are not using the default composer
   420  	// root directory to avoid confusion.
   421  	if app.ComposerRoot != "" && showWarning {
   422  		util.Warning("Using '%s' as composer root directory", absComposerRoot)
   423  	}
   424  
   425  	return absComposerRoot
   426  }
   427  
   428  // GetName returns the app's name
   429  func (app *DdevApp) GetName() string {
   430  	return app.Name
   431  }
   432  
   433  // GetPhpVersion returns the app's php version
   434  func (app *DdevApp) GetPhpVersion() string {
   435  	v := nodeps.PHPDefault
   436  	if app.PHPVersion != "" {
   437  		v = app.PHPVersion
   438  	}
   439  	return v
   440  }
   441  
   442  // GetWebserverType returns the app's webserver type (nginx-fpm/apache-fpm)
   443  func (app *DdevApp) GetWebserverType() string {
   444  	v := nodeps.WebserverDefault
   445  	if app.WebserverType != "" {
   446  		v = app.WebserverType
   447  	}
   448  	return v
   449  }
   450  
   451  // ImportDB takes a source sql dump and imports it to an active site's database container.
   452  func (app *DdevApp) ImportDB(imPath string, extPath string, progress bool, noDrop bool, targetDB string) error {
   453  	app.DockerEnv()
   454  	dockerutil.CheckAvailableSpace()
   455  
   456  	if targetDB == "" {
   457  		targetDB = "db"
   458  	}
   459  	var extPathPrompt bool
   460  	dbPath, err := os.MkdirTemp(filepath.Dir(app.ConfigPath), ".importdb")
   461  	if err != nil {
   462  		return err
   463  	}
   464  	err = os.Chmod(dbPath, 0777)
   465  	if err != nil {
   466  		return err
   467  	}
   468  
   469  	defer func() {
   470  		_ = os.RemoveAll(dbPath)
   471  	}()
   472  
   473  	err = app.ProcessHooks("pre-import-db")
   474  	if err != nil {
   475  		return err
   476  	}
   477  
   478  	// If they don't provide an import path and we're not on a tty (piped in stuff)
   479  	// then prompt for path to db
   480  	if imPath == "" && isatty.IsTerminal(os.Stdin.Fd()) {
   481  		// ensure we prompt for extraction path if an archive is provided, while still allowing
   482  		// non-interactive use of --src flag without providing a --extract-path flag.
   483  		if extPath == "" {
   484  			extPathPrompt = true
   485  		}
   486  		output.UserOut.Println("Provide the path to the database you want to import.")
   487  		fmt.Print("Path to file: ")
   488  
   489  		imPath = util.GetInput("")
   490  	}
   491  
   492  	if imPath != "" {
   493  		importPath, isArchive, err := appimport.ValidateAsset(imPath, "db")
   494  		if err != nil {
   495  			if isArchive && extPathPrompt {
   496  				output.UserOut.Println("You provided an archive. Do you want to extract from a specific path in your archive? You may leave this blank if you wish to use the full archive contents")
   497  				fmt.Print("Archive extraction path:")
   498  
   499  				extPath = util.GetInput("")
   500  			} else {
   501  				return fmt.Errorf("Unable to validate import asset %s: %s", imPath, err)
   502  			}
   503  		}
   504  
   505  		switch {
   506  		case strings.HasSuffix(importPath, "sql.gz") || strings.HasSuffix(importPath, "mysql.gz"):
   507  			err = archive.Ungzip(importPath, dbPath)
   508  			if err != nil {
   509  				return fmt.Errorf("failed to extract provided file: %v", err)
   510  			}
   511  
   512  		case strings.HasSuffix(importPath, "sql.bz2") || strings.HasSuffix(importPath, "mysql.bz2"):
   513  			err = archive.UnBzip2(importPath, dbPath)
   514  			if err != nil {
   515  				return fmt.Errorf("failed to extract file: %v", err)
   516  			}
   517  
   518  		case strings.HasSuffix(importPath, "sql.xz") || strings.HasSuffix(importPath, "mysql.xz"):
   519  			err = archive.UnXz(importPath, dbPath)
   520  			if err != nil {
   521  				return fmt.Errorf("failed to extract file: %v", err)
   522  			}
   523  
   524  		case strings.HasSuffix(importPath, "zip"):
   525  			err = archive.Unzip(importPath, dbPath, extPath)
   526  			if err != nil {
   527  				return fmt.Errorf("failed to extract provided archive: %v", err)
   528  			}
   529  
   530  		case strings.HasSuffix(importPath, "tar"):
   531  			fallthrough
   532  		case strings.HasSuffix(importPath, "tar.gz"):
   533  			fallthrough
   534  		case strings.HasSuffix(importPath, "tar.bz2"):
   535  			fallthrough
   536  		case strings.HasSuffix(importPath, "tar.xz"):
   537  			fallthrough
   538  		case strings.HasSuffix(importPath, "tgz"):
   539  			err := archive.Untar(importPath, dbPath, extPath)
   540  			if err != nil {
   541  				return fmt.Errorf("failed to extract provided archive: %v", err)
   542  			}
   543  
   544  		default:
   545  			err = fileutil.CopyFile(importPath, filepath.Join(dbPath, "db.sql"))
   546  			if err != nil {
   547  				return err
   548  			}
   549  		}
   550  
   551  		matches, err := filepath.Glob(filepath.Join(dbPath, "*.*sql"))
   552  		if err != nil {
   553  			return err
   554  		}
   555  
   556  		if len(matches) < 1 {
   557  			return fmt.Errorf("no .sql or .mysql files found to import")
   558  		}
   559  	}
   560  
   561  	// default insideContainerImportPath is the one mounted from .ddev directory
   562  	insideContainerImportPath := path.Join("/mnt/ddev_config/", filepath.Base(dbPath))
   563  	// But if we don't have bind mounts, we have to copy dump into the container
   564  	if globalconfig.DdevGlobalConfig.NoBindMounts {
   565  		dbContainerName := GetContainerName(app, "db")
   566  		if err != nil {
   567  			return err
   568  		}
   569  		uid, _, _ := util.GetContainerUIDGid()
   570  		// for postgres, must be written with postgres user
   571  		if app.Database.Type == nodeps.Postgres {
   572  			uid = "999"
   573  		}
   574  
   575  		insideContainerImportPath, _, err = dockerutil.Exec(dbContainerName, "mktemp -d", uid)
   576  		if err != nil {
   577  			return err
   578  		}
   579  		insideContainerImportPath = strings.Trim(insideContainerImportPath, "\n")
   580  
   581  		err = dockerutil.CopyIntoContainer(dbPath, dbContainerName, insideContainerImportPath, "")
   582  		if err != nil {
   583  			return err
   584  		}
   585  	}
   586  
   587  	err = app.MutagenSyncFlush()
   588  	if err != nil {
   589  		return err
   590  	}
   591  	// The perl manipulation removes statements like CREATE DATABASE and USE, which
   592  	// throw off imports. This is a scary manipulation, as it must not match actual content
   593  	// as has actually happened with https://www.ddevhq.org/ddev-local/ddev-local-database-management/
   594  	// and in https://github.com/drud/ddev/issues/2787
   595  	// The backtick after USE is inserted via fmt.Sprintf argument because it seems there's
   596  	// no way to escape a backtick in a string literal.
   597  	inContainerCommand := []string{}
   598  	preImportSQL := ""
   599  	switch app.Database.Type {
   600  	case nodeps.MySQL:
   601  		fallthrough
   602  	case nodeps.MariaDB:
   603  		preImportSQL = fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s; GRANT ALL ON %s.* TO 'db'@'%%';", targetDB, targetDB)
   604  		if !noDrop {
   605  			preImportSQL = fmt.Sprintf("DROP DATABASE IF EXISTS %s; ", targetDB) + preImportSQL
   606  		}
   607  
   608  		// Case for reading from file
   609  		inContainerCommand = []string{"bash", "-c", fmt.Sprintf(`set -eu -o pipefail && mysql -uroot -proot -e "%s" && pv %s/*.*sql |  perl -p -e 's/^(CREATE DATABASE \/\*|USE %s)[^;]*;//' | mysql %s`, preImportSQL, insideContainerImportPath, "`", targetDB)}
   610  
   611  		// Alternate case where we are reading from stdin
   612  		if imPath == "" && extPath == "" {
   613  			inContainerCommand = []string{"bash", "-c", fmt.Sprintf(`set -eu -o pipefail && mysql -uroot -proot -e "%s" && perl -p -e 's/^(CREATE DATABASE \/\*|USE %s)[^;]*;//' | mysql %s`, preImportSQL, "`", targetDB)}
   614  		}
   615  
   616  	case nodeps.Postgres:
   617  		preImportSQL = ""
   618  		if !noDrop { // Normal case, drop and recreate database
   619  			preImportSQL = preImportSQL + fmt.Sprintf(`
   620  				DROP DATABASE IF EXISTS %s;
   621  				CREATE DATABASE %s;
   622  			`, targetDB, targetDB)
   623  		} else { // Leave database alone, but create if not exists
   624  			preImportSQL = preImportSQL + fmt.Sprintf(`
   625  				SELECT 'CREATE DATABASE %s' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '%s')\gexec
   626  			`, targetDB, targetDB)
   627  		}
   628  		preImportSQL = preImportSQL + fmt.Sprintf(`
   629  			GRANT ALL PRIVILEGES ON DATABASE %s TO db;`, targetDB)
   630  
   631  		// If there is no import path, we're getting it from stdin
   632  		if imPath == "" && extPath == "" {
   633  			inContainerCommand = []string{"bash", "-c", fmt.Sprintf(`set -eu -o pipefail && (echo '%s' | psql -d postgres) && psql -v ON_ERROR_STOP=1 -d %s`, preImportSQL, targetDB)}
   634  		} else { // otherwise getting it from mounted file
   635  			inContainerCommand = []string{"bash", "-c", fmt.Sprintf(`set -eu -o pipefail && (echo "%s" | psql -q -d postgres -v ON_ERROR_STOP=1) && pv %s/*.*sql | psql -q -v ON_ERROR_STOP=1 %s >/dev/null`, preImportSQL, insideContainerImportPath, targetDB)}
   636  		}
   637  	}
   638  	stdout, stderr, err := app.Exec(&ExecOpts{
   639  		Service: "db",
   640  		RawCmd:  inContainerCommand,
   641  		Tty:     progress && isatty.IsTerminal(os.Stdin.Fd()),
   642  	})
   643  
   644  	if err != nil {
   645  		return fmt.Errorf("failed to import database: %v\nstdout: %s\nstderr: %s", err, stdout, stderr)
   646  	}
   647  
   648  	// Wait for import to really complete
   649  	if app.Database.Type != nodeps.Postgres {
   650  		rowsImported := 0
   651  		for i := 0; i < 10; i++ {
   652  
   653  			stdout, _, err := app.Exec(&ExecOpts{
   654  				Cmd:     `mysqladmin -uroot -proot extended -r 2>/dev/null | awk -F'|' '/Innodb_rows_inserted/ {print $3}'`,
   655  				Service: "db",
   656  			})
   657  			if err != nil {
   658  				util.Warning("mysqladmin command failed: %v", err)
   659  			}
   660  			stdout = strings.Trim(stdout, "\r\n\t ")
   661  			newRowsImported, err := strconv.Atoi(stdout)
   662  			if err != nil {
   663  				util.Warning("Error converting '%s' to int", stdout)
   664  				break
   665  			}
   666  			// See if mysqld is still importing. If it is, sleep and try again
   667  			if newRowsImported == rowsImported {
   668  				break
   669  			} else {
   670  				rowsImported = newRowsImported
   671  				time.Sleep(time.Millisecond * 500)
   672  			}
   673  		}
   674  	}
   675  
   676  	_, err = app.CreateSettingsFile()
   677  	if err != nil {
   678  		util.Warning("A custom settings file exists for your application, so ddev did not generate one.")
   679  		util.Warning("Run 'ddev describe' to find the database credentials for this application.")
   680  	}
   681  
   682  	err = app.PostImportDBAction()
   683  	if err != nil {
   684  		return fmt.Errorf("failed to execute PostImportDBAction: %v", err)
   685  	}
   686  
   687  	err = fileutil.PurgeDirectory(dbPath)
   688  	if err != nil {
   689  		return fmt.Errorf("failed to clean up %s after import: %v", dbPath, err)
   690  	}
   691  
   692  	err = app.ProcessHooks("post-import-db")
   693  	if err != nil {
   694  		return err
   695  	}
   696  
   697  	return nil
   698  }
   699  
   700  // ExportDB exports the db, with optional output to a file, default gzip
   701  // targetDB is the db name if not default "db"
   702  func (app *DdevApp) ExportDB(outFile string, compressionType string, targetDB string) error {
   703  	app.DockerEnv()
   704  	exportCmd := []string{"mysqldump"}
   705  	if app.Database.Type == "postgres" {
   706  		exportCmd = []string{"pg_dump", "-U", "db"}
   707  	}
   708  	if targetDB == "" {
   709  		targetDB = "db"
   710  	}
   711  	exportCmd = append(exportCmd, targetDB)
   712  
   713  	if compressionType != "" {
   714  		exportCmd = []string{"bash", "-c", fmt.Sprintf(`set -eu -o pipefail; %s | %s`, strings.Join(exportCmd, " "), compressionType)}
   715  	}
   716  
   717  	opts := &ExecOpts{
   718  		Service:   "db",
   719  		RawCmd:    exportCmd,
   720  		NoCapture: true,
   721  	}
   722  	if outFile != "" {
   723  		f, err := os.OpenFile(outFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
   724  		if err != nil {
   725  			return fmt.Errorf("failed to open %s: %v", outFile, err)
   726  		}
   727  		opts.Stdout = f
   728  		defer func() {
   729  			_ = f.Close()
   730  		}()
   731  	}
   732  	stdout, stderr, err := app.Exec(opts)
   733  
   734  	if err != nil {
   735  		return fmt.Errorf("unable to export db: %v\nstdout: %s\nstderr: %s", err, stdout, stderr)
   736  	}
   737  
   738  	confMsg := "Wrote database dump from project '" + app.Name + "' database '" + targetDB + "'"
   739  	if outFile != "" {
   740  		confMsg = confMsg + " to file " + outFile
   741  	} else {
   742  		confMsg = confMsg + " to stdout"
   743  	}
   744  	if compressionType != "" {
   745  		confMsg = fmt.Sprintf("%s in %s format", confMsg, compressionType)
   746  	} else {
   747  		confMsg = confMsg + " in plain text format"
   748  	}
   749  
   750  	_, err = fmt.Fprintf(os.Stderr, confMsg+".\n")
   751  
   752  	return err
   753  }
   754  
   755  // SiteStatus returns the current status of an application determined from web and db service health.
   756  // returns status, statusDescription
   757  func (app *DdevApp) SiteStatus() (string, string) {
   758  	if !fileutil.FileExists(app.GetAppRoot()) {
   759  		return SiteDirMissing, fmt.Sprintf(`%s: %v; Please "ddev stop --unlist %s"`, SiteDirMissing, app.GetAppRoot(), app.Name)
   760  	}
   761  
   762  	_, err := CheckForConf(app.GetAppRoot())
   763  	if err != nil {
   764  		return SiteConfigMissing, fmt.Sprintf("%s", SiteConfigMissing)
   765  	}
   766  
   767  	statuses := map[string]string{"web": ""}
   768  	if !nodeps.ArrayContainsString(app.GetOmittedContainers(), "db") {
   769  		statuses["db"] = ""
   770  	}
   771  
   772  	for service := range statuses {
   773  		container, err := app.FindContainerByType(service)
   774  		if err != nil {
   775  			util.Error("app.FindContainerByType(%v) failed", service)
   776  			return "", ""
   777  		}
   778  		if container == nil {
   779  			statuses[service] = SiteStopped
   780  		} else {
   781  			status, _ := dockerutil.GetContainerHealth(container)
   782  
   783  			switch status {
   784  			case "exited":
   785  				statuses[service] = SitePaused
   786  			case "healthy":
   787  				statuses[service] = SiteRunning
   788  			case "starting":
   789  				statuses[service] = SiteStarting
   790  			default:
   791  				statuses[service] = status
   792  			}
   793  		}
   794  	}
   795  
   796  	siteStatusDesc := ""
   797  	for serviceName, status := range statuses {
   798  		if status != statuses["web"] {
   799  			siteStatusDesc += serviceName + ": " + status + "\n"
   800  		}
   801  	}
   802  
   803  	// Base the siteStatus on web container. Then override it if others are not the same.
   804  	if siteStatusDesc == "" {
   805  		return app.determineStatus(statuses), statuses["web"]
   806  	}
   807  
   808  	return app.determineStatus(statuses), siteStatusDesc
   809  }
   810  
   811  // Return one of the Site* statuses to describe the overall status of the project
   812  func (app *DdevApp) determineStatus(statuses map[string]string) string {
   813  	hasCommonStatus, commonStatus := app.getCommonStatus(statuses)
   814  
   815  	if hasCommonStatus {
   816  		return commonStatus
   817  	}
   818  
   819  	for status := range statuses {
   820  		if status == SiteStarting {
   821  			return SiteStarting
   822  		}
   823  	}
   824  
   825  	return SiteUnhealthy
   826  }
   827  
   828  // Check whether a common status applies to all services
   829  func (app *DdevApp) getCommonStatus(statuses map[string]string) (bool, string) {
   830  	commonStatus := ""
   831  
   832  	for _, status := range statuses {
   833  		if commonStatus != "" && status != commonStatus {
   834  			return false, ""
   835  		}
   836  
   837  		commonStatus = status
   838  	}
   839  
   840  	return true, commonStatus
   841  }
   842  
   843  // ImportFiles takes a source directory or archive and copies to the uploaded files directory of a given app.
   844  func (app *DdevApp) ImportFiles(importPath string, extPath string) error {
   845  	app.DockerEnv()
   846  
   847  	if err := app.ProcessHooks("pre-import-files"); err != nil {
   848  		return err
   849  	}
   850  
   851  	if err := app.ImportFilesAction(importPath, extPath); err != nil {
   852  		return err
   853  	}
   854  
   855  	//nolint: revive
   856  	if err := app.ProcessHooks("post-import-files"); err != nil {
   857  		return err
   858  	}
   859  
   860  	return nil
   861  }
   862  
   863  // ComposeFiles returns a list of compose files for a project.
   864  // It has to put the .ddev/docker-compose.*.y*ml first
   865  // It has to put the docker-compose.override.y*l last
   866  func (app *DdevApp) ComposeFiles() ([]string, error) {
   867  	origDir, _ := os.Getwd()
   868  	defer func() {
   869  		_ = os.Chdir(origDir)
   870  	}()
   871  	err := os.Chdir(app.AppConfDir())
   872  	if err != nil {
   873  		return nil, err
   874  	}
   875  	files, err := filepath.Glob("docker-compose.*.y*ml")
   876  	if err != nil {
   877  		return []string{}, fmt.Errorf("unable to glob docker-compose.*.y*ml in %s: err=%v", app.AppConfDir(), err)
   878  	}
   879  
   880  	mainFile := app.DockerComposeYAMLPath()
   881  	if !fileutil.FileExists(mainFile) {
   882  		return nil, fmt.Errorf("failed to find %s", mainFile)
   883  	}
   884  
   885  	overrides, err := filepath.Glob("docker-compose.override.y*ml")
   886  	util.CheckErr(err)
   887  
   888  	orderedFiles := make([]string, 1)
   889  
   890  	// Make sure the main file goes first
   891  	orderedFiles[0] = mainFile
   892  
   893  	for _, file := range files {
   894  		// We already have the main file, and it's not in the list anyway, so skip when we hit it.
   895  		// We'll add the override later, so skip it.
   896  		if len(overrides) == 1 && file == overrides[0] {
   897  			continue
   898  		}
   899  		orderedFiles = append(orderedFiles, app.GetConfigPath(file))
   900  	}
   901  	if len(overrides) == 1 {
   902  		orderedFiles = append(orderedFiles, app.GetConfigPath(overrides[0]))
   903  	}
   904  	return orderedFiles, nil
   905  }
   906  
   907  // ProcessHooks executes Tasks defined in Hooks
   908  func (app *DdevApp) ProcessHooks(hookName string) error {
   909  	if cmds := app.Hooks[hookName]; len(cmds) > 0 {
   910  		output.UserOut.Debugf("Executing %s hook...", hookName)
   911  	}
   912  
   913  	for _, c := range app.Hooks[hookName] {
   914  		a := NewTask(app, c)
   915  		if a == nil {
   916  			return fmt.Errorf("unable to create task from %v", c)
   917  		}
   918  
   919  		if hookName == "pre-start" {
   920  			for k := range c {
   921  				if k == "exec" || k == "composer" {
   922  					return fmt.Errorf("pre-start hooks cannot contain %v", k)
   923  				}
   924  			}
   925  		}
   926  
   927  		output.UserOut.Debugf("=== Running task: %s, output below", a.GetDescription())
   928  
   929  		err := a.Execute()
   930  
   931  		if err != nil {
   932  			if app.FailOnHookFail || app.FailOnHookFailGlobal {
   933  				output.UserOut.Errorf("Task failed: %v: %v", a.GetDescription(), err)
   934  				return fmt.Errorf("task failed: %v", err)
   935  			}
   936  			output.UserOut.Errorf("Task failed: %v: %v", a.GetDescription(), err)
   937  			output.UserOut.Warn("A task failure does not mean that ddev failed, but your hook configuration has a command that failed.")
   938  		}
   939  	}
   940  
   941  	return nil
   942  }
   943  
   944  // GetDBImage uses the available version info
   945  func (app *DdevApp) GetDBImage() string {
   946  	dbImage := versionconstants.GetDBImage(app.Database.Type, app.Database.Version)
   947  	return dbImage
   948  }
   949  
   950  // Start initiates docker-compose up
   951  func (app *DdevApp) Start() error {
   952  	var err error
   953  
   954  	if app.IsMutagenEnabled() && globalconfig.DdevGlobalConfig.UseHardenedImages {
   955  		return fmt.Errorf("mutagen-enabled is not compatible with use-hardened-images")
   956  	}
   957  	app.DockerEnv()
   958  	dockerutil.EnsureDdevNetwork()
   959  
   960  	if err = dockerutil.CheckDockerCompose(); err != nil {
   961  		util.Failed(`Your docker-compose version does not exist or is set to an invalid version. 
   962  Please use the built-in docker-compose.
   963  Fix with 'ddev config global --required-docker-compose-version="" --use-docker-compose-from-path=false': %v`, err)
   964  	}
   965  
   966  	if !nodeps.ArrayContainsString(app.GetOmittedContainers(), "db") {
   967  		// OK to start if dbType is empty (nonexistent) or if it matches
   968  		if dbType, err := app.GetExistingDBType(); err != nil || (dbType != "" && dbType != app.Database.Type+":"+app.Database.Version) {
   969  			return fmt.Errorf("Unable to start project %s because the configured database type does not match the current actual database. Please change your database type back to %s and start again, export, delete, and then change configuration and start. To get back to existing type use 'ddev config --database=%s' and then you might want to try 'ddev debug migrate-database %s', see docs at %s", app.Name, dbType, dbType, app.Database.Type+":"+app.Database.Version, "https://ddev.readthedocs.io/en/latest/users/extend/database-types/")
   970  		}
   971  	}
   972  
   973  	if app.IsMutagenEnabled() {
   974  		if ok, volumeExists, info := CheckMutagenVolumeSyncCompatibility(app); !ok {
   975  			util.Debug("mutagen sync session and docker volume are in incompatible status: '%s', Removing mutagen sync session '%s' and docker volume %s", info, MutagenSyncName(app.Name), GetMutagenVolumeName(app))
   976  			terminateErr := TerminateMutagenSync(app)
   977  			if terminateErr != nil {
   978  				util.Warning("Unable to terminate mutagen sync %s: %v", MutagenSyncName(app.Name), err)
   979  			}
   980  			if volumeExists {
   981  				removeVolumeErr := dockerutil.RemoveVolume(GetMutagenVolumeName(app))
   982  				if removeVolumeErr != nil {
   983  					return fmt.Errorf(`Unable to remove mismatched mutagen docker volume '%s'. Please use 'ddev restart' or 'ddev mutagen reset': %v`, GetMutagenVolumeName(app), removeVolumeErr)
   984  				}
   985  			}
   986  		}
   987  		// Check again to make sure the mutagen docker volume exists. It's compatible if we found it above
   988  		// so we can keep it in that case.
   989  		if !dockerutil.VolumeExists(GetMutagenVolumeName(app)) {
   990  			_, err = dockerutil.CreateVolume(GetMutagenVolumeName(app), "local", nil, map[string]string{mutagenSignatureLabelName: GetDefaultMutagenVolumeSignature(app)})
   991  			if err != nil {
   992  				return fmt.Errorf("Unable to create new mutagen docker volume %s: %v", GetMutagenVolumeName(app), err)
   993  			}
   994  		}
   995  	}
   996  
   997  	volumesNeeded := []string{"ddev-global-cache", "ddev-" + app.Name + "-snapshots"}
   998  	for _, v := range volumesNeeded {
   999  		_, err = dockerutil.CreateVolume(v, "local", nil, nil)
  1000  		if err != nil {
  1001  			return fmt.Errorf("unable to create docker volume %s: %v", v, err)
  1002  		}
  1003  	}
  1004  
  1005  	err = app.CheckExistingAppInApproot()
  1006  	if err != nil {
  1007  		return err
  1008  	}
  1009  
  1010  	// This is done early here so users won't see gitignored contents of .ddev for too long
  1011  	// It also gets done by `ddev config`
  1012  	err = PrepDdevDirectory(app)
  1013  	if err != nil {
  1014  		util.Warning("Unable to PrepDdevDirectory: %v", err)
  1015  	}
  1016  
  1017  	err = PopulateCustomCommandFiles(app)
  1018  	if err != nil {
  1019  		util.Warning("Failed to populate custom command files: %v", err)
  1020  	}
  1021  
  1022  	// The .ddev directory may still need to be populated, especially in tests
  1023  	err = PopulateExamplesCommandsHomeadditions(app.Name)
  1024  	if err != nil {
  1025  		return err
  1026  	}
  1027  	// Make sure that any ports allocated are available.
  1028  	// and of course add to global project list as well
  1029  	err = app.UpdateGlobalProjectList()
  1030  	if err != nil {
  1031  		return err
  1032  	}
  1033  
  1034  	err = DownloadMutagenIfNeeded(app)
  1035  	if err != nil {
  1036  		return err
  1037  	}
  1038  
  1039  	err = app.ProcessHooks("pre-start")
  1040  	if err != nil {
  1041  		return err
  1042  	}
  1043  
  1044  	err = app.GenerateWebserverConfig()
  1045  	if err != nil {
  1046  		return err
  1047  	}
  1048  
  1049  	err = app.GeneratePostgresConfig()
  1050  	if err != nil {
  1051  		return err
  1052  	}
  1053  
  1054  	err = app.PullBaseContainerImages()
  1055  	if err != nil {
  1056  		util.Warning("Unable to pull docker images: %v", err)
  1057  	}
  1058  
  1059  	dockerutil.CheckAvailableSpace()
  1060  
  1061  	// Copy any homeadditions content into .ddev/.homeadditions
  1062  	tmpHomeadditionsPath := app.GetConfigPath(".homeadditions")
  1063  	err = os.RemoveAll(tmpHomeadditionsPath)
  1064  	if err != nil {
  1065  		util.Warning("unable to remove %s: %v", tmpHomeadditionsPath, err)
  1066  	}
  1067  	globalHomeadditionsPath := filepath.Join(globalconfig.GetGlobalDdevDir(), "homeadditions")
  1068  	if fileutil.IsDirectory(globalHomeadditionsPath) {
  1069  		err = copy.Copy(globalHomeadditionsPath, tmpHomeadditionsPath, copy.Options{OnSymlink: func(string) copy.SymlinkAction { return copy.Deep }})
  1070  		if err != nil {
  1071  			return err
  1072  		}
  1073  	}
  1074  	projectHomeAdditionsPath := app.GetConfigPath("homeadditions")
  1075  	if fileutil.IsDirectory(projectHomeAdditionsPath) {
  1076  		err = copy.Copy(projectHomeAdditionsPath, tmpHomeadditionsPath, copy.Options{OnSymlink: func(string) copy.SymlinkAction { return copy.Deep }})
  1077  		if err != nil {
  1078  			return err
  1079  		}
  1080  	}
  1081  
  1082  	// Make sure that important volumes to mount already have correct ownership set
  1083  	// Additional volumes can be added here. This allows us to run a single privileged
  1084  	// container with a single focus of changing ownership, instead of having to use sudo
  1085  	// inside the container
  1086  	uid, _, _ := util.GetContainerUIDGid()
  1087  
  1088  	if globalconfig.DdevGlobalConfig.NoBindMounts {
  1089  		err = dockerutil.CopyIntoVolume(app.GetConfigPath(""), app.Name+"-ddev-config", "", uid, "db_snapshots", true)
  1090  		if err != nil {
  1091  			return fmt.Errorf("failed to copy project .ddev directory to volume: %v", err)
  1092  		}
  1093  	}
  1094  
  1095  	_, out, err := dockerutil.RunSimpleContainer(versionconstants.GetWebImage(), "", []string{"sh", "-c", fmt.Sprintf("chown -R %s /var/lib/mysql /mnt/ddev-global-cache", uid)}, []string{}, []string{}, []string{app.GetMariaDBVolumeName() + ":/var/lib/mysql", "ddev-global-cache:/mnt/ddev-global-cache"}, "", true, false, nil)
  1096  	if err != nil {
  1097  		return fmt.Errorf("failed to RunSimpleContainer to chown volumes: %v, output=%s", err, out)
  1098  	}
  1099  
  1100  	// Chown the postgres volume; this shouldn't have to be a separate stanza, but the
  1101  	// uid is 999 instead of current user
  1102  	if app.Database.Type == nodeps.Postgres {
  1103  		_, out, err := dockerutil.RunSimpleContainer(versionconstants.GetWebImage(), "", []string{"sh", "-c", fmt.Sprintf("chown -R %s /var/lib/postgresql/data", "999:999")}, []string{}, []string{}, []string{app.GetPostgresVolumeName() + ":/var/lib/postgresql/data"}, "", true, false, nil)
  1104  		if err != nil {
  1105  			return fmt.Errorf("failed to RunSimpleContainer to chown postgres volume: %v, output=%s", err, out)
  1106  		}
  1107  	}
  1108  
  1109  	if !nodeps.ArrayContainsString(app.GetOmittedContainers(), "ddev-ssh-agent") {
  1110  		err = app.EnsureSSHAgentContainer()
  1111  		if err != nil {
  1112  			return err
  1113  		}
  1114  	}
  1115  
  1116  	// Warn the user if there is any custom configuration in use.
  1117  	app.CheckCustomConfig()
  1118  
  1119  	// Warn user if there are deprecated items used in the config
  1120  	app.CheckDeprecations()
  1121  
  1122  	// Fix any obsolete things like old shell commands, etc.
  1123  	app.FixObsolete()
  1124  
  1125  	app.CreateUploadDirIfNecessary()
  1126  
  1127  	// WriteConfig .ddev-docker-compose-*.yaml
  1128  	err = app.WriteDockerComposeYAML()
  1129  	if err != nil {
  1130  		return err
  1131  	}
  1132  
  1133  	// This needs to be done after WriteDockerComposeYAML() to get the right images
  1134  	err = app.PullContainerImages()
  1135  	if err != nil {
  1136  		util.Warning("Unable to pull docker images: %v", err)
  1137  	}
  1138  
  1139  	err = app.CheckAddonIncompatibilities()
  1140  	if err != nil {
  1141  		return err
  1142  	}
  1143  
  1144  	err = app.AddHostsEntriesIfNeeded()
  1145  	if err != nil {
  1146  		return err
  1147  	}
  1148  
  1149  	// Delete the NFS volumes before we bring up docker-compose (and will be created again)
  1150  	// We don't care if the volume wasn't there
  1151  	_ = dockerutil.RemoveVolume(app.GetNFSMountVolumeName())
  1152  
  1153  	// The db_snapshots subdirectory may be created on docker-compose up, so
  1154  	// we need to precreate it so permissions are correct (and not root:root)
  1155  	if !fileutil.IsDirectory(app.GetConfigPath("db_snapshots")) {
  1156  		err = os.MkdirAll(app.GetConfigPath("db_snapshots"), 0777)
  1157  		if err != nil {
  1158  			return err
  1159  		}
  1160  	}
  1161  	// db_snapshots gets mounted into container, may have different user/group, so need 777
  1162  	err = os.Chmod(app.GetConfigPath("db_snapshots"), 0777)
  1163  	if err != nil {
  1164  		return err
  1165  	}
  1166  
  1167  	util.Debug("Executing docker-compose -f %s up --build -d", app.DockerComposeFullRenderedYAMLPath())
  1168  	_, _, err = dockerutil.ComposeCmd([]string{app.DockerComposeFullRenderedYAMLPath()}, "up", "--build", "-d")
  1169  	if err != nil {
  1170  		return err
  1171  	}
  1172  
  1173  	if !IsRouterDisabled(app) {
  1174  		caRoot := globalconfig.GetCAROOT()
  1175  		if caRoot == "" {
  1176  			util.Warning("mkcert may not be properly installed, we suggest installing it for trusted https support, `brew install mkcert nss`, `choco install -y mkcert`, etc. and then `mkcert -install`")
  1177  		}
  1178  		router, _ := FindDdevRouter()
  1179  
  1180  		// If the router doesn't exist, go ahead and push mkcert root ca certs into the ddev-global-cache/mkcert
  1181  		// This will often be redundant
  1182  		if router == nil {
  1183  			// Copy ca certs into ddev-global-cache/mkcert
  1184  			if caRoot != "" {
  1185  				uid, _, _ := util.GetContainerUIDGid()
  1186  				err = dockerutil.CopyIntoVolume(caRoot, "ddev-global-cache", "mkcert", uid, "", false)
  1187  				if err != nil {
  1188  					util.Warning("failed to copy root CA into docker volume ddev-global-cache/mkcert: %v", err)
  1189  				} else {
  1190  					util.Debug("Pushed mkcert rootca certs to ddev-global-cache/mkcert")
  1191  				}
  1192  			}
  1193  
  1194  		}
  1195  
  1196  		// If TLS supported and using traefik, create cert/key and push into ddev-global-cache/traefik
  1197  		if globalconfig.DdevGlobalConfig.UseTraefik {
  1198  			err = configureTraefikForApp(app)
  1199  			if err != nil {
  1200  				return err
  1201  			}
  1202  		}
  1203  
  1204  		// Push custom certs
  1205  		targetSubdir := "custom_certs"
  1206  		if globalconfig.DdevGlobalConfig.UseTraefik {
  1207  			targetSubdir = path.Join("traefik", "certs")
  1208  		}
  1209  		certPath := app.GetConfigPath("custom_certs")
  1210  		uid, _, _ := util.GetContainerUIDGid()
  1211  		if fileutil.FileExists(certPath) && globalconfig.DdevGlobalConfig.MkcertCARoot != "" {
  1212  			err = dockerutil.CopyIntoVolume(certPath, "ddev-global-cache", targetSubdir, uid, "", false)
  1213  			if err != nil {
  1214  				util.Warning("failed to copy custom certs into docker volume ddev-global-cache/custom_certs: %v", err)
  1215  			} else {
  1216  				util.Debug("Installed custom cert from %s", certPath)
  1217  			}
  1218  		}
  1219  	}
  1220  
  1221  	if app.IsMutagenEnabled() {
  1222  		CheckMutagenUploadDir(app)
  1223  		// Must wait for web container to be healthy before fiddling with mutagen
  1224  		err = app.Wait([]string{"web"})
  1225  		if err != nil {
  1226  			return fmt.Errorf("web container failed to become ready: %v", err)
  1227  		}
  1228  
  1229  		mounted, err := IsMutagenVolumeMounted(app)
  1230  		if err != nil {
  1231  			return err
  1232  		}
  1233  		if !mounted {
  1234  			util.Failed("Mutagen docker volume is not mounted. Please use `ddev restart`")
  1235  		}
  1236  		output.UserOut.Printf("Starting mutagen sync process... This can take some time.")
  1237  		mutagenDuration := util.ElapsedDuration(time.Now())
  1238  		err = app.GenerateMutagenYml()
  1239  		if err != nil {
  1240  			return err
  1241  		}
  1242  
  1243  		err = SetMutagenVolumeOwnership(app)
  1244  		if err != nil {
  1245  			return err
  1246  		}
  1247  		err = CreateOrResumeMutagenSync(app)
  1248  		if err != nil {
  1249  			return fmt.Errorf("Failed to create mutagen sync session '%s'. You may be able to resolve this problem using 'ddev mutagen reset' (err=%v)", MutagenSyncName(app.Name), err)
  1250  		}
  1251  		mStatus, _, _, err := app.MutagenStatus()
  1252  		if err != nil {
  1253  			return err
  1254  		}
  1255  
  1256  		dur := util.FormatDuration(mutagenDuration())
  1257  		if mStatus == "ok" {
  1258  			util.Success("Mutagen sync flush completed in %s.\nFor details on sync status 'ddev mutagen st %s -l'", dur, MutagenSyncName(app.Name))
  1259  		} else {
  1260  			util.Error("Mutagen sync completed with problems in %s.\nFor details on sync status 'ddev mutagen st %s -l'", dur, MutagenSyncName(app.Name))
  1261  		}
  1262  		err = fileutil.TemplateStringToFile(`#ddev-generated`, nil, app.GetConfigPath("mutagen/.start-synced"))
  1263  		if err != nil {
  1264  			util.Warning("could not create file %s: %v", app.GetConfigPath("mutagen/.start-synced"), err)
  1265  		}
  1266  	}
  1267  
  1268  	// Wait for web/db containers to become healthy
  1269  	dependers := []string{"web"}
  1270  	if !nodeps.ArrayContainsString(app.GetOmittedContainers(), "db") {
  1271  		dependers = append(dependers, "db")
  1272  	}
  1273  	err = app.Wait(dependers)
  1274  	if err != nil {
  1275  		util.Warning("Failed waiting for web/db containers to become ready: %v", err)
  1276  	}
  1277  
  1278  	// WebExtraDaemons have to be started after mutagen sync is done, because so often
  1279  	// they depend on code being synced into the container/volume
  1280  	if len(app.WebExtraDaemons) > 0 {
  1281  		util.Debug("Starting web_extra_daaemons")
  1282  		stdout, stderr, err := app.Exec(&ExecOpts{
  1283  			Cmd: `supervisorctl start webextradaemons:*`,
  1284  		})
  1285  		if err != nil {
  1286  			util.Warning("Unable to start web_extra_daemons using supervisorctl, stdout=%s, stderr=%s: %v", stdout, stderr, err)
  1287  		}
  1288  	}
  1289  
  1290  	util.Debug("Testing to see if /mnt/ddev_config is properly mounted")
  1291  	_, _, err = app.Exec(&ExecOpts{
  1292  		Cmd: `ls -l /mnt/ddev_config/nginx_full/nginx-site.conf >/dev/null`,
  1293  	})
  1294  	if err != nil {
  1295  		util.Warning("Something is wrong with docker or colima and /mnt/ddev_config is not mounted from the project .ddev folder. This can cause all kinds of problems.")
  1296  	}
  1297  
  1298  	if !IsRouterDisabled(app) {
  1299  		err = StartDdevRouter()
  1300  		if err != nil {
  1301  			return err
  1302  		}
  1303  	}
  1304  
  1305  	err = app.WaitByLabels(map[string]string{"com.ddev.site-name": app.GetName()})
  1306  	if err != nil {
  1307  		return err
  1308  	}
  1309  
  1310  	if _, err = app.CreateSettingsFile(); err != nil {
  1311  		return fmt.Errorf("failed to write settings file %s: %v", app.SiteDdevSettingsFile, err)
  1312  	}
  1313  
  1314  	err = app.PostStartAction()
  1315  	if err != nil {
  1316  		return err
  1317  	}
  1318  
  1319  	err = app.ProcessHooks("post-start")
  1320  	if err != nil {
  1321  		return err
  1322  	}
  1323  
  1324  	return nil
  1325  }
  1326  
  1327  // Restart does a Stop() and a Start
  1328  func (app *DdevApp) Restart() error {
  1329  	err := app.Stop(false, false)
  1330  	if err != nil {
  1331  		return err
  1332  	}
  1333  	err = app.Start()
  1334  	return err
  1335  }
  1336  
  1337  // PullContainerImages configured docker images with full output, since docker-compose up doesn't have nice output
  1338  func (app *DdevApp) PullContainerImages() error {
  1339  	images, err := app.FindAllImages()
  1340  	if err != nil {
  1341  		return err
  1342  	}
  1343  
  1344  	images = append(images, versionconstants.GetRouterImage(), versionconstants.GetSSHAuthImage())
  1345  	for _, i := range images {
  1346  		err := dockerutil.Pull(i)
  1347  		if err != nil {
  1348  			return err
  1349  		}
  1350  		util.Debug("Pulling image for %s", i)
  1351  	}
  1352  
  1353  	return nil
  1354  }
  1355  
  1356  // PullCBaseontainerImages pulls only the fundamentally needed images so they can be available early
  1357  // We always need web image and busybox just for housekeeping.
  1358  func (app *DdevApp) PullBaseContainerImages() error {
  1359  	images := []string{versionconstants.GetWebImage(), versionconstants.BusyboxImage}
  1360  	if !nodeps.ArrayContainsString(app.GetOmittedContainers(), SSHAuthName) {
  1361  		images = append(images, versionconstants.GetSSHAuthImage())
  1362  	}
  1363  	if !nodeps.ArrayContainsString(app.GetOmittedContainers(), RouterProjectName) {
  1364  		images = append(images, versionconstants.GetRouterImage())
  1365  	}
  1366  
  1367  	for _, i := range images {
  1368  		err := dockerutil.Pull(i)
  1369  		if err != nil {
  1370  			return err
  1371  		}
  1372  		util.Debug("Pulling image for %s", i)
  1373  	}
  1374  
  1375  	return nil
  1376  }
  1377  
  1378  // FindAllImages returns an array of image tags for all containers in the compose file
  1379  func (app *DdevApp) FindAllImages() ([]string, error) {
  1380  	var images []string
  1381  	if app.ComposeYaml == nil {
  1382  		return images, nil
  1383  	}
  1384  	if y, ok := app.ComposeYaml["services"]; ok {
  1385  		for _, v := range y.(map[string]interface{}) {
  1386  			if i, ok := v.(map[string]interface{})["image"]; ok {
  1387  				if strings.HasSuffix(i.(string), "-built") {
  1388  					i = strings.TrimSuffix(i.(string), "-built")
  1389  					if strings.HasSuffix(i.(string), "-"+app.Name) {
  1390  						i = strings.TrimSuffix(i.(string), "-"+app.Name)
  1391  					}
  1392  				}
  1393  				images = append(images, i.(string))
  1394  			}
  1395  		}
  1396  	}
  1397  
  1398  	return images, nil
  1399  }
  1400  
  1401  // FindMaxTimeout looks through all services and returns the max timeout found
  1402  // Defaults to 120
  1403  func (app *DdevApp) FindMaxTimeout() int {
  1404  	const defaultContainerTimeout = 120
  1405  	maxTimeout := defaultContainerTimeout
  1406  	if app.ComposeYaml == nil {
  1407  		return defaultContainerTimeout
  1408  	}
  1409  	if y, ok := app.ComposeYaml["services"]; ok {
  1410  		for _, v := range y.(map[string]interface{}) {
  1411  			if i, ok := v.(map[string]interface{})["healthcheck"]; ok {
  1412  				if timeout, ok := i.(map[string]interface{})["timeout"]; ok {
  1413  					duration, err := time.ParseDuration(timeout.(string))
  1414  					if err != nil {
  1415  						continue
  1416  					}
  1417  					t := int(duration.Seconds())
  1418  					if t > maxTimeout {
  1419  						maxTimeout = t
  1420  					}
  1421  				}
  1422  			}
  1423  		}
  1424  	}
  1425  	return maxTimeout
  1426  }
  1427  
  1428  // CheckExistingAppInApproot looks to see if we already have a project in this approot with different name
  1429  func (app *DdevApp) CheckExistingAppInApproot() error {
  1430  	pList := globalconfig.GetGlobalProjectList()
  1431  	for name, v := range pList {
  1432  		if app.AppRoot == v.AppRoot && name != app.Name {
  1433  			return fmt.Errorf(`this project root %s already contains a project named %s. You may want to remove the existing project with "ddev stop --unlist %s"`, v.AppRoot, name, name)
  1434  		}
  1435  	}
  1436  	return nil
  1437  }
  1438  
  1439  //go:embed webserver_config_assets
  1440  var webserverConfigAssets embed.FS
  1441  
  1442  // GenerateWebserverConfig generates the default nginx and apache config files
  1443  func (app *DdevApp) GenerateWebserverConfig() error {
  1444  	// Prevent running as root for most cases
  1445  	// We really don't want ~/.ddev to have root ownership, breaks things.
  1446  	if os.Geteuid() == 0 {
  1447  		util.Warning("not generating webserver config files because running with root privileges")
  1448  		return nil
  1449  	}
  1450  
  1451  	var items = map[string]string{
  1452  		"nginx":                         app.GetConfigPath(filepath.Join("nginx_full", "nginx-site.conf")),
  1453  		"apache":                        app.GetConfigPath(filepath.Join("apache", "apache-site.conf")),
  1454  		"nginx_second_docroot_example":  app.GetConfigPath(filepath.Join("nginx_full", "seconddocroot.conf.example")),
  1455  		"README.nginx_full.txt":         app.GetConfigPath(filepath.Join("nginx_full", "README.nginx_full.txt")),
  1456  		"README.apache.txt":             app.GetConfigPath(filepath.Join("apache", "README.apache.txt")),
  1457  		"apache_second_docroot_example": app.GetConfigPath(filepath.Join("apache", "seconddocroot.conf.example")),
  1458  	}
  1459  	for t, configPath := range items {
  1460  		err := os.MkdirAll(filepath.Dir(configPath), 0755)
  1461  		if err != nil {
  1462  			return err
  1463  		}
  1464  
  1465  		if fileutil.FileExists(configPath) {
  1466  			sigExists, err := fileutil.FgrepStringInFile(configPath, nodeps.DdevFileSignature)
  1467  			if err != nil {
  1468  				return err
  1469  			}
  1470  			// If the signature doesn't exist, they have taken over the file, so return
  1471  			if !sigExists {
  1472  				return nil
  1473  			}
  1474  		}
  1475  
  1476  		cfgFile := fmt.Sprintf("%s-site-%s.conf", t, app.Type)
  1477  		c, err := webserverConfigAssets.ReadFile(path.Join("webserver_config_assets", cfgFile))
  1478  		if err != nil {
  1479  			c, err = webserverConfigAssets.ReadFile(path.Join("webserver_config_assets", fmt.Sprintf("%s-site-php.conf", t)))
  1480  			if err != nil {
  1481  				return err
  1482  			}
  1483  		}
  1484  		content := string(c)
  1485  		docroot := path.Join("/var/www/html", app.Docroot)
  1486  		err = fileutil.TemplateStringToFile(content, map[string]interface{}{"Docroot": docroot}, configPath)
  1487  		if err != nil {
  1488  			return err
  1489  		}
  1490  	}
  1491  	return nil
  1492  }
  1493  
  1494  func (app *DdevApp) GeneratePostgresConfig() error {
  1495  	if app.Database.Type != nodeps.Postgres {
  1496  		return nil
  1497  	}
  1498  	// Prevent running as root for most cases
  1499  	// We really don't want ~/.ddev to have root ownership, breaks things.
  1500  	if os.Geteuid() == 0 {
  1501  		util.Warning("not generating postgres config files because running with root privileges")
  1502  		return nil
  1503  	}
  1504  
  1505  	var items = map[string]string{
  1506  		"postgresql.conf": app.GetConfigPath(filepath.Join("postgres", "postgresql.conf")),
  1507  	}
  1508  	for _, configPath := range items {
  1509  		err := os.MkdirAll(filepath.Dir(configPath), 0755)
  1510  		if err != nil {
  1511  			return err
  1512  		}
  1513  
  1514  		if fileutil.FileExists(configPath) {
  1515  			err = os.Chmod(configPath, 0666)
  1516  			if err != nil {
  1517  				return err
  1518  			}
  1519  			sigExists, err := fileutil.FgrepStringInFile(configPath, nodeps.DdevFileSignature)
  1520  			if err != nil {
  1521  				return err
  1522  			}
  1523  			// If the signature doesn't exist, they have taken over the file, so return
  1524  			if !sigExists {
  1525  				return nil
  1526  			}
  1527  		}
  1528  
  1529  		c, err := bundledAssets.ReadFile(path.Join("postgres", app.Database.Version, "postgresql.conf"))
  1530  		if err != nil {
  1531  			return err
  1532  		}
  1533  		err = fileutil.TemplateStringToFile(string(c), nil, configPath)
  1534  		if err != nil {
  1535  			return err
  1536  		}
  1537  		err = os.Chmod(configPath, 0666)
  1538  		if err != nil {
  1539  			return err
  1540  		}
  1541  	}
  1542  	return nil
  1543  }
  1544  
  1545  // ExecOpts contains options for running a command inside a container
  1546  type ExecOpts struct {
  1547  	// Service is the service, as in 'web', 'db', 'dba'
  1548  	Service string
  1549  	// Dir is the full path to the working directory inside the container
  1550  	Dir string
  1551  	// Cmd is the string to execute via bash/sh
  1552  	Cmd string
  1553  	// RawCmd is the array to execute if not using
  1554  	RawCmd []string
  1555  	// Nocapture if true causes use of ComposeNoCapture, so the stdout and stderr go right to stdout/stderr
  1556  	NoCapture bool
  1557  	// Tty if true causes a tty to be allocated
  1558  	Tty bool
  1559  	// Stdout can be overridden with a File
  1560  	Stdout *os.File
  1561  	// Stderr can be overridden with a File
  1562  	Stderr *os.File
  1563  }
  1564  
  1565  // Exec executes a given command in the container of given type without allocating a pty
  1566  // Returns ComposeCmd results of stdout, stderr, err
  1567  // If Nocapture arg is true, stdout/stderr will be empty and output directly to stdout/stderr
  1568  func (app *DdevApp) Exec(opts *ExecOpts) (string, string, error) {
  1569  	app.DockerEnv()
  1570  
  1571  	runTime := util.TimeTrack(time.Now(), fmt.Sprintf("app.Exec %v", opts))
  1572  	defer runTime()
  1573  
  1574  	if opts.Cmd == "" && len(opts.RawCmd) == 0 {
  1575  		return "", "", fmt.Errorf("no command provided")
  1576  	}
  1577  
  1578  	if opts.Service == "" {
  1579  		opts.Service = "web"
  1580  	}
  1581  
  1582  	state, err := dockerutil.GetContainerStateByName(fmt.Sprintf("ddev-%s-%s", app.Name, opts.Service))
  1583  	if err != nil || state != "running" {
  1584  		if state == "doesnotexist" {
  1585  			return "", "", fmt.Errorf("service %s does not exist in project %s (state=%s)", opts.Service, app.Name, state)
  1586  		}
  1587  		return "", "", fmt.Errorf("service %s is not currently running in project %s (state=%s), use `ddev logs -s %s` to see what happened to it", opts.Service, app.Name, state, opts.Service)
  1588  	}
  1589  
  1590  	err = app.ProcessHooks("pre-exec")
  1591  	if err != nil {
  1592  		return "", "", fmt.Errorf("failed to process pre-exec hooks: %v", err)
  1593  	}
  1594  
  1595  	baseComposeExecCmd := []string{"exec"}
  1596  	if opts.Dir != "" {
  1597  		baseComposeExecCmd = append(baseComposeExecCmd, "-w", opts.Dir)
  1598  	}
  1599  
  1600  	if !isatty.IsTerminal(os.Stdin.Fd()) || !opts.Tty {
  1601  		baseComposeExecCmd = append(baseComposeExecCmd, "-T")
  1602  	}
  1603  
  1604  	baseComposeExecCmd = append(baseComposeExecCmd, opts.Service)
  1605  
  1606  	// Cases to handle
  1607  	// - Free form, all unquoted. Like `ls -l -a`
  1608  	// - Quoted to delay pipes and other features to container, like `"ls -l -a | grep junk"`
  1609  	// Note that a set quoted on the host in ddev e will come through as a single arg
  1610  
  1611  	if len(opts.RawCmd) == 0 { // Use opts.Cmd and prepend with bash
  1612  		// Use bash for our containers, sh for 3rd-party containers
  1613  		// that may not have bash.
  1614  		shell := "bash"
  1615  		if !nodeps.ArrayContainsString([]string{"web", "db", "dba"}, opts.Service) {
  1616  			shell = "sh"
  1617  		}
  1618  		errcheck := "set -eu"
  1619  		opts.RawCmd = []string{shell, "-c", errcheck + ` && ( ` + opts.Cmd + `)`}
  1620  	}
  1621  	files := []string{app.DockerComposeFullRenderedYAMLPath()}
  1622  	if err != nil {
  1623  		return "", "", err
  1624  	}
  1625  
  1626  	stdout := os.Stdout
  1627  	stderr := os.Stderr
  1628  	if opts.Stdout != nil {
  1629  		stdout = opts.Stdout
  1630  	}
  1631  	if opts.Stderr != nil {
  1632  		stderr = opts.Stderr
  1633  	}
  1634  
  1635  	var stdoutResult, stderrResult string
  1636  	var outRes, errRes string
  1637  	r := append(baseComposeExecCmd, opts.RawCmd...)
  1638  	if opts.NoCapture || opts.Tty {
  1639  		err = dockerutil.ComposeWithStreams(files, os.Stdin, stdout, stderr, r...)
  1640  	} else {
  1641  		outRes, errRes, err = dockerutil.ComposeCmd([]string{app.DockerComposeFullRenderedYAMLPath()}, r...)
  1642  		stdoutResult = outRes
  1643  		stderrResult = errRes
  1644  	}
  1645  	if err != nil {
  1646  		return stdoutResult, stderrResult, err
  1647  	}
  1648  	hookErr := app.ProcessHooks("post-exec")
  1649  	if hookErr != nil {
  1650  		return stdoutResult, stderrResult, fmt.Errorf("failed to process post-exec hooks: %v", hookErr)
  1651  	}
  1652  	return stdoutResult, stderrResult, err
  1653  }
  1654  
  1655  // ExecWithTty executes a given command in the container of given type.
  1656  // It allocates a pty for interactive work.
  1657  func (app *DdevApp) ExecWithTty(opts *ExecOpts) error {
  1658  	app.DockerEnv()
  1659  
  1660  	if opts.Service == "" {
  1661  		opts.Service = "web"
  1662  	}
  1663  
  1664  	state, err := dockerutil.GetContainerStateByName(fmt.Sprintf("ddev-%s-%s", app.Name, opts.Service))
  1665  	if err != nil || state != "running" {
  1666  		return fmt.Errorf("service %s is not current running in project %s (state=%s)", opts.Service, app.Name, state)
  1667  	}
  1668  
  1669  	args := []string{"exec"}
  1670  
  1671  	// In the case where this is being used without an available tty,
  1672  	// make sure we use the -T to turn off tty to avoid panic in docker-compose v2.2.3
  1673  	// see https://stackoverflow.com/questions/70855915/fix-panic-provided-file-is-not-a-console-from-docker-compose-in-github-action
  1674  	if !term.IsTerminal(int(os.Stdin.Fd())) {
  1675  		args = append(args, "-T")
  1676  	}
  1677  
  1678  	if opts.Dir != "" {
  1679  		args = append(args, "-w", opts.Dir)
  1680  	}
  1681  
  1682  	args = append(args, opts.Service)
  1683  
  1684  	if opts.Cmd == "" {
  1685  		return fmt.Errorf("no command provided")
  1686  	}
  1687  
  1688  	// Cases to handle
  1689  	// - Free form, all unquoted. Like `ls -l -a`
  1690  	// - Quoted to delay pipes and other features to container, like `"ls -l -a | grep junk"`
  1691  	// Note that a set quoted on the host in ddev exec will come through as a single arg
  1692  
  1693  	// Use bash for our containers, sh for 3rd-party containers
  1694  	// that may not have bash.
  1695  	shell := "bash"
  1696  	if !nodeps.ArrayContainsString([]string{"web", "db", "dba"}, opts.Service) {
  1697  		shell = "sh"
  1698  	}
  1699  	args = append(args, shell, "-c", opts.Cmd)
  1700  
  1701  	return dockerutil.ComposeWithStreams([]string{app.DockerComposeFullRenderedYAMLPath()}, os.Stdin, os.Stdout, os.Stderr, args...)
  1702  }
  1703  
  1704  func (app *DdevApp) ExecOnHostOrService(service string, cmd string) error {
  1705  	var err error
  1706  	// Handle case on host
  1707  	if service == "host" {
  1708  		cwd, _ := os.Getwd()
  1709  		err = os.Chdir(app.GetAppRoot())
  1710  		if err != nil {
  1711  			return fmt.Errorf("unable to GetAppRoot: %v", err)
  1712  		}
  1713  		bashPath := "bash"
  1714  		if runtime.GOOS == "windows" {
  1715  			bashPath = util.FindBashPath()
  1716  			if bashPath == "" {
  1717  				return fmt.Errorf("unable to find bash.exe on Windows")
  1718  			}
  1719  		}
  1720  
  1721  		args := []string{
  1722  			"-c",
  1723  			cmd,
  1724  		}
  1725  
  1726  		app.DockerEnv()
  1727  		err = exec.RunInteractiveCommand(bashPath, args)
  1728  		_ = os.Chdir(cwd)
  1729  	} else { // handle case in container
  1730  		_, _, err = app.Exec(
  1731  			&ExecOpts{
  1732  				Service: service,
  1733  				Cmd:     cmd,
  1734  				Tty:     isatty.IsTerminal(os.Stdin.Fd()),
  1735  			})
  1736  	}
  1737  	return err
  1738  }
  1739  
  1740  // Logs returns logs for a site's given container.
  1741  // See docker.LogsOptions for more information about valid tailLines values.
  1742  func (app *DdevApp) Logs(service string, follow bool, timestamps bool, tailLines string) error {
  1743  	client := dockerutil.GetDockerClient()
  1744  
  1745  	var container *docker.APIContainers
  1746  	var err error
  1747  	// Let people access ddev-router and ddev-ssh-agent logs as well.
  1748  	if service == "ddev-router" || service == "ddev-ssh-agent" {
  1749  		container, err = dockerutil.FindContainerByLabels(map[string]string{"com.docker.compose.service": service})
  1750  	} else {
  1751  		container, err = app.FindContainerByType(service)
  1752  	}
  1753  	if err != nil {
  1754  		return err
  1755  	}
  1756  	if container == nil {
  1757  		util.Warning("No running service container %s was found", service)
  1758  		return nil
  1759  	}
  1760  
  1761  	logOpts := docker.LogsOptions{
  1762  		Container:    container.ID,
  1763  		Stdout:       true,
  1764  		Stderr:       true,
  1765  		OutputStream: output.UserOut.Out,
  1766  		ErrorStream:  output.UserOut.Out,
  1767  		Follow:       follow,
  1768  		Timestamps:   timestamps,
  1769  	}
  1770  
  1771  	if tailLines != "" {
  1772  		logOpts.Tail = tailLines
  1773  	}
  1774  
  1775  	err = client.Logs(logOpts)
  1776  	if err != nil {
  1777  		return err
  1778  	}
  1779  
  1780  	return nil
  1781  }
  1782  
  1783  // CaptureLogs returns logs for a site's given container.
  1784  // See docker.LogsOptions for more information about valid tailLines values.
  1785  func (app *DdevApp) CaptureLogs(service string, timestamps bool, tailLines string) (string, error) {
  1786  	client := dockerutil.GetDockerClient()
  1787  
  1788  	var container *docker.APIContainers
  1789  	var err error
  1790  	// Let people access ddev-router and ddev-ssh-agent logs as well.
  1791  	if service == "ddev-router" || service == "ddev-ssh-agent" {
  1792  		container, err = dockerutil.FindContainerByLabels(map[string]string{"com.docker.compose.service": service})
  1793  	} else {
  1794  		container, err = app.FindContainerByType(service)
  1795  	}
  1796  	if err != nil {
  1797  		return "", err
  1798  	}
  1799  	if container == nil {
  1800  		util.Warning("No running service container %s was found", service)
  1801  		return "", nil
  1802  	}
  1803  
  1804  	var out bytes.Buffer
  1805  
  1806  	logOpts := docker.LogsOptions{
  1807  		Container:    container.ID,
  1808  		Stdout:       true,
  1809  		Stderr:       true,
  1810  		OutputStream: &out,
  1811  		ErrorStream:  &out,
  1812  		Follow:       false,
  1813  		Timestamps:   timestamps,
  1814  	}
  1815  
  1816  	if tailLines != "" {
  1817  		logOpts.Tail = tailLines
  1818  	}
  1819  
  1820  	err = client.Logs(logOpts)
  1821  	if err != nil {
  1822  		return "", err
  1823  	}
  1824  
  1825  	return out.String(), nil
  1826  }
  1827  
  1828  // DockerEnv sets environment variables for a docker-compose run.
  1829  func (app *DdevApp) DockerEnv() {
  1830  
  1831  	uidStr, gidStr, _ := util.GetContainerUIDGid()
  1832  
  1833  	// Warn about running as root if we're not on Windows.
  1834  	if uidStr == "0" || gidStr == "0" {
  1835  		util.Warning("Warning: containers will run as root. This could be a security risk on Linux.")
  1836  	}
  1837  
  1838  	// For gitpod, codespaces
  1839  	// * provide default host-side port bindings, assuming only one project running,
  1840  	//   as is usual on gitpod, but if more than one project, can override with normal
  1841  	//   config.yaml settings.
  1842  	if nodeps.IsGitpod() || nodeps.IsCodespaces() {
  1843  		if app.HostWebserverPort == "" {
  1844  			app.HostWebserverPort = "8080"
  1845  		}
  1846  		if app.HostHTTPSPort == "" {
  1847  			app.HostHTTPSPort = "8443"
  1848  		}
  1849  		if app.HostDBPort == "" {
  1850  			app.HostDBPort = "3306"
  1851  		}
  1852  		if app.HostMailhogPort == "" {
  1853  			app.HostMailhogPort = "8027"
  1854  		}
  1855  		if app.HostPHPMyAdminPort == "" {
  1856  			app.HostPHPMyAdminPort = "8036"
  1857  		}
  1858  		app.BindAllInterfaces = true
  1859  	}
  1860  	isWSL2 := "false"
  1861  	if dockerutil.IsWSL2() {
  1862  		isWSL2 = "true"
  1863  	}
  1864  
  1865  	// DDEV_HOST_DB_PORT is actually used for 2 things.
  1866  	// 1. To specify via base docker-compose file the value of host_db_port config. And it's expected to be empty
  1867  	//    there if the host_db_port is empty.
  1868  	// 2. To tell custom commands the db port. And it's expected always to be populated for them.
  1869  	dbPort, err := app.GetPublishedPort("db")
  1870  	dbPortStr := strconv.Itoa(dbPort)
  1871  	if dbPortStr == "-1" || err != nil {
  1872  		dbPortStr = ""
  1873  	}
  1874  	if app.HostDBPort != "" {
  1875  		dbPortStr = app.HostDBPort
  1876  	}
  1877  
  1878  	envVars := map[string]string{
  1879  		// Without COMPOSE_DOCKER_CLI_BUILD=0, docker-compose makes all kinds of mess
  1880  		// of output. BUILDKIT_PROGRESS doesn't help either.
  1881  		"COMPOSE_DOCKER_CLI_BUILD":       "0",
  1882  		"COMPOSER_EXIT_ON_PATCH_FAILURE": "1",
  1883  		// The compose project name can no longer contain dots
  1884  		// https://github.com/compose-spec/compose-go/pull/197
  1885  		"COMPOSE_PROJECT_NAME":          "ddev-" + strings.Replace(app.Name, `.`, "", -1),
  1886  		"COMPOSE_CONVERT_WINDOWS_PATHS": "true",
  1887  		"DDEV_SITENAME":                 app.Name,
  1888  		"DDEV_TLD":                      app.ProjectTLD,
  1889  		"DDEV_DBIMAGE":                  app.GetDBImage(),
  1890  		"DDEV_DBAIMAGE":                 versionconstants.GetDBAImage(),
  1891  		"DDEV_PROJECT":                  app.Name,
  1892  		"DDEV_WEBIMAGE":                 app.WebImage,
  1893  		"DDEV_APPROOT":                  app.AppRoot,
  1894  		"DDEV_COMPOSER_ROOT":            app.GetComposerRoot(true, false),
  1895  		"DDEV_DATABASE":                 app.Database.Type + ":" + app.Database.Version,
  1896  		"DDEV_FILES_DIR":                app.GetContainerUploadDirFullPath(),
  1897  
  1898  		"DDEV_HOST_DB_PORT":          dbPortStr,
  1899  		"DDEV_HOST_MAILHOG_PORT":     app.HostMailhogPort,
  1900  		"DDEV_HOST_WEBSERVER_PORT":   app.HostWebserverPort,
  1901  		"DDEV_HOST_HTTPS_PORT":       app.HostHTTPSPort,
  1902  		"DDEV_PHPMYADMIN_PORT":       app.PHPMyAdminPort,
  1903  		"DDEV_PHPMYADMIN_HTTPS_PORT": app.PHPMyAdminHTTPSPort,
  1904  		"DDEV_MAILHOG_PORT":          app.MailhogPort,
  1905  		"DDEV_MAILHOG_HTTPS_PORT":    app.MailhogHTTPSPort,
  1906  		"DDEV_DOCROOT":               app.Docroot,
  1907  		"DDEV_HOSTNAME":              app.HostName(),
  1908  		"DDEV_UID":                   uidStr,
  1909  		"DDEV_GID":                   gidStr,
  1910  		"DDEV_MUTAGEN_ENABLED":       strconv.FormatBool(app.IsMutagenEnabled()),
  1911  		"DDEV_PHP_VERSION":           app.PHPVersion,
  1912  		"DDEV_WEBSERVER_TYPE":        app.WebserverType,
  1913  		"DDEV_PROJECT_TYPE":          app.Type,
  1914  		"DDEV_ROUTER_HTTP_PORT":      app.RouterHTTPPort,
  1915  		"DDEV_ROUTER_HTTPS_PORT":     app.RouterHTTPSPort,
  1916  		"DDEV_XDEBUG_ENABLED":        strconv.FormatBool(app.XdebugEnabled),
  1917  		"DDEV_PRIMARY_URL":           app.GetPrimaryURL(),
  1918  		"DDEV_VERSION":               versionconstants.DdevVersion,
  1919  		"DOCKER_SCAN_SUGGEST":        "false",
  1920  		"GOOS":                       runtime.GOOS,
  1921  		"GOARCH":                     runtime.GOARCH,
  1922  		"IS_DDEV_PROJECT":            "true",
  1923  		"IS_GITPOD":                  strconv.FormatBool(nodeps.IsGitpod()),
  1924  		"IS_CODESPACES":              strconv.FormatBool(nodeps.IsCodespaces()),
  1925  		"IS_WSL2":                    isWSL2,
  1926  	}
  1927  
  1928  	// Set the DDEV_DB_CONTAINER_COMMAND command to empty to prevent docker-compose from complaining normally.
  1929  	// It's used for special startup on restoring to a snapshot or for postgres.
  1930  	if len(os.Getenv("DDEV_DB_CONTAINER_COMMAND")) == 0 {
  1931  		v := ""
  1932  		if app.Database.Type == nodeps.Postgres { // config_file spec for postgres
  1933  			v = fmt.Sprintf("-c config_file=%s/postgresql.conf -c hba_file=%s/pg_hba.conf", nodeps.PostgresConfigDir, nodeps.PostgresConfigDir)
  1934  		}
  1935  		envVars["DDEV_DB_CONTAINER_COMMAND"] = v
  1936  	}
  1937  
  1938  	// Find out terminal dimensions
  1939  	columns, lines := nodeps.GetTerminalWidthHeight()
  1940  
  1941  	envVars["COLUMNS"] = strconv.Itoa(columns)
  1942  	envVars["LINES"] = strconv.Itoa(lines)
  1943  
  1944  	if len(app.AdditionalHostnames) > 0 || len(app.AdditionalFQDNs) > 0 {
  1945  		envVars["DDEV_HOSTNAME"] = strings.Join(app.GetHostnames(), ",")
  1946  	}
  1947  
  1948  	// Only set values if they don't already exist in env.
  1949  	for k, v := range envVars {
  1950  		if err := os.Setenv(k, v); err != nil {
  1951  			util.Error("Failed to set the environment variable %s=%s: %v", k, v, err)
  1952  		}
  1953  	}
  1954  }
  1955  
  1956  // Pause initiates docker-compose stop
  1957  func (app *DdevApp) Pause() error {
  1958  	app.DockerEnv()
  1959  
  1960  	status, _ := app.SiteStatus()
  1961  	if status == SiteStopped {
  1962  		return nil
  1963  	}
  1964  
  1965  	err := app.ProcessHooks("pre-pause")
  1966  	if err != nil {
  1967  		return err
  1968  	}
  1969  
  1970  	_ = SyncAndPauseMutagenSession(app)
  1971  
  1972  	if _, _, err := dockerutil.ComposeCmd([]string{app.DockerComposeFullRenderedYAMLPath()}, "stop"); err != nil {
  1973  		return err
  1974  	}
  1975  	err = app.ProcessHooks("post-pause")
  1976  	if err != nil {
  1977  		return err
  1978  	}
  1979  
  1980  	return StopRouterIfNoContainers()
  1981  }
  1982  
  1983  // WaitForServices waits for all the services in docker-compose to come up
  1984  func (app *DdevApp) WaitForServices() error {
  1985  	var requiredContainers []string
  1986  	if services, ok := app.ComposeYaml["services"].(map[string]interface{}); ok {
  1987  		for k := range services {
  1988  			requiredContainers = append(requiredContainers, k)
  1989  		}
  1990  	} else {
  1991  		util.Failed("unable to get required startup services to wait for")
  1992  	}
  1993  	output.UserOut.Printf("Waiting for these services to become ready: %v", requiredContainers)
  1994  
  1995  	labels := map[string]string{
  1996  		"com.ddev.site-name": app.GetName(),
  1997  	}
  1998  	waitTime := app.FindMaxTimeout()
  1999  	_, err := dockerutil.ContainerWait(waitTime, labels)
  2000  	if err != nil {
  2001  		return fmt.Errorf("timed out waiting for containers (%v) to start: err=%v", requiredContainers, err)
  2002  	}
  2003  	return nil
  2004  }
  2005  
  2006  // Wait ensures that the app service containers are healthy.
  2007  func (app *DdevApp) Wait(requiredContainers []string) error {
  2008  	for _, containerType := range requiredContainers {
  2009  		labels := map[string]string{
  2010  			"com.ddev.site-name":         app.GetName(),
  2011  			"com.docker.compose.service": containerType,
  2012  		}
  2013  		waitTime := app.FindMaxTimeout()
  2014  		logOutput, err := dockerutil.ContainerWait(waitTime, labels)
  2015  		if err != nil {
  2016  			return fmt.Errorf("%s container failed: log=%s, err=%v", containerType, logOutput, err)
  2017  		}
  2018  	}
  2019  
  2020  	return nil
  2021  }
  2022  
  2023  // WaitByLabels waits for containers found by list of labels to be
  2024  // ready
  2025  func (app *DdevApp) WaitByLabels(labels map[string]string) error {
  2026  	waitTime := app.FindMaxTimeout()
  2027  	err := dockerutil.ContainersWait(waitTime, labels)
  2028  	if err != nil {
  2029  		return fmt.Errorf("container(s) failed to become healthy before their configured timeout or in %d seconds. This may be just a problem with the healthcheck and not a functional problem. (%v)", waitTime, err)
  2030  	}
  2031  	return nil
  2032  }
  2033  
  2034  // StartAndWait is primarily for use in tests.
  2035  // It does app.Start() but then waits for extra seconds
  2036  // before returning.
  2037  // extraSleep arg in seconds is the time to wait if > 0
  2038  func (app *DdevApp) StartAndWait(extraSleep int) error {
  2039  	err := app.Start()
  2040  	if err != nil {
  2041  		return err
  2042  	}
  2043  	if extraSleep > 0 {
  2044  		time.Sleep(time.Duration(extraSleep) * time.Second)
  2045  	}
  2046  	return nil
  2047  }
  2048  
  2049  // DetermineSettingsPathLocation figures out the path to the settings file for
  2050  // an app based on the contents/existence of app.SiteSettingsPath and
  2051  // app.SiteDdevSettingsFile.
  2052  func (app *DdevApp) DetermineSettingsPathLocation() (string, error) {
  2053  	possibleLocations := []string{app.SiteSettingsPath, app.SiteDdevSettingsFile}
  2054  	for _, loc := range possibleLocations {
  2055  		// If the file doesn't exist, it's safe to use
  2056  		if !fileutil.FileExists(loc) {
  2057  			return loc, nil
  2058  		}
  2059  
  2060  		// If the file does exist, check for a signature indicating it's managed by ddev.
  2061  		signatureFound, err := fileutil.FgrepStringInFile(loc, nodeps.DdevFileSignature)
  2062  		util.CheckErr(err) // Really can't happen as we already checked for the file existence
  2063  
  2064  		// If the signature was found, it's safe to use.
  2065  		if signatureFound {
  2066  			return loc, nil
  2067  		}
  2068  	}
  2069  
  2070  	return "", fmt.Errorf("settings files already exist and are being managed by the user")
  2071  }
  2072  
  2073  // Snapshot causes a snapshot of the db to be written into the snapshots volume
  2074  // Returns the name of the snapshot and err
  2075  func (app *DdevApp) Snapshot(snapshotName string) (string, error) {
  2076  	containerSnapshotDirBase := "/var/tmp"
  2077  
  2078  	err := app.ProcessHooks("pre-snapshot")
  2079  	if err != nil {
  2080  		return "", fmt.Errorf("failed to process pre-stop hooks: %v", err)
  2081  	}
  2082  
  2083  	if snapshotName == "" {
  2084  		t := time.Now()
  2085  		snapshotName = app.Name + "_" + t.Format("20060102150405")
  2086  	}
  2087  
  2088  	snapshotFile := snapshotName + "-" + app.Database.Type + "_" + app.Database.Version + ".gz"
  2089  
  2090  	existingSnapshots, err := app.ListSnapshots()
  2091  	if err != nil {
  2092  		return "", err
  2093  	}
  2094  	if nodeps.ArrayContainsString(existingSnapshots, snapshotName) {
  2095  		return "", fmt.Errorf("snapshot %s already exists, please use another snapshot name or clean up snapshots with `ddev snapshot --cleanup`", snapshotFile)
  2096  	}
  2097  
  2098  	// Container side has to use path.Join instead of filepath.Join because they are
  2099  	// targeted at the linux filesystem, so won't work with filepath on Windows
  2100  	containerSnapshotDir := containerSnapshotDirBase
  2101  
  2102  	// Ensure that db container is up.
  2103  	err = app.Wait([]string{"db"})
  2104  	if err != nil {
  2105  		return "", fmt.Errorf("unable to snapshot database, \nyour db container in project %v is not running. \nPlease start the project if you want to snapshot it. \nIf deleting project, you can delete without a snapshot using \n'ddev delete --omit-snapshot --yes', \nwhich will destroy your database", app.Name)
  2106  	}
  2107  
  2108  	// For versions less than 8.0.32, we have to OPTIMIZE TABLES to make xtrabackup work
  2109  	// See https://docs.percona.com/percona-xtrabackup/8.0/em/instant.html and
  2110  	// https://www.percona.com/blog/percona-xtrabackup-8-0-29-and-instant-add-drop-columns/
  2111  	if app.Database.Type == "mysql" && app.Database.Version == nodeps.MySQL80 {
  2112  		stdout, stderr, err := app.Exec(&ExecOpts{
  2113  			Service: "db",
  2114  			Cmd:     `set -eu -o pipefail; MYSQL_PWD=root mysql -e 'SET SQL_NOTES=0'; mysql -N -uroot -e 'SELECT NAME FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE TOTAL_ROW_VERSIONS > 0;'`,
  2115  		})
  2116  		if err != nil {
  2117  			util.Warning("could not check for tables to optimize (mysql 8.0): %v (stdout='%s', stderr='%s')", err, stdout, stderr)
  2118  		} else {
  2119  			stdout = strings.Trim(stdout, "\n\t ")
  2120  			tables := strings.Split(stdout, "\n")
  2121  			// util.Success("tables=%v len(tables)=%d stdout was '%s'", tables, len(tables), stdout)
  2122  			if len(stdout) > 0 && len(tables) > 0 {
  2123  				for _, t := range tables {
  2124  					r := strings.Split(t, `/`)
  2125  					if len(r) != 2 {
  2126  						util.Warning("unable to get database/table from %s", r)
  2127  						continue
  2128  					}
  2129  					d := r[0]
  2130  					t := r[1]
  2131  					stdout, stderr, err := app.Exec(&ExecOpts{
  2132  						Service: "db",
  2133  						Cmd:     fmt.Sprintf(`set -eu -o pipefail; MYSQL_PWD=root mysql -uroot -D %s -e 'OPTIMIZE TABLES %s';`, d, t),
  2134  					})
  2135  					if err != nil {
  2136  						util.Warning("unable to optimize table %s (mysql 8.0): %v (stdout='%s', stderr='%s')", t, err, stdout, stderr)
  2137  					}
  2138  				}
  2139  				util.Success("Optimized mysql 8.0 tables '%s' in preparation for snapshot", strings.Join(tables, `,'`))
  2140  			}
  2141  		}
  2142  	}
  2143  	util.Success("Creating database snapshot %s", snapshotName)
  2144  
  2145  	c := getBackupCommand(app, path.Join(containerSnapshotDir, snapshotFile))
  2146  	stdout, stderr, err := app.Exec(&ExecOpts{
  2147  		Service: "db",
  2148  		Cmd:     fmt.Sprintf(`set -eu -o pipefail; %s `, c),
  2149  	})
  2150  
  2151  	if err != nil {
  2152  		util.Warning("Failed to create snapshot: %v, stdout=%s, stderr=%s", err, stdout, stderr)
  2153  		return "", err
  2154  	}
  2155  
  2156  	dbContainer, err := GetContainer(app, "db")
  2157  	if err != nil {
  2158  		return "", err
  2159  	}
  2160  
  2161  	if globalconfig.DdevGlobalConfig.NoBindMounts {
  2162  		// If we're not using bind-mounts, we have to copy the snapshot back into
  2163  		// the host project's .ddev/db_snapshots directory
  2164  		elapsed := util.TimeTrack(time.Now(), "CopySnapshotFromContainer")
  2165  		// Copy snapshot back to the host
  2166  		err = dockerutil.CopyFromContainer(GetContainerName(app, "db"), path.Join(containerSnapshotDir, snapshotFile), app.GetConfigPath("db_snapshots"))
  2167  		if err != nil {
  2168  			return "", err
  2169  		}
  2170  		elapsed()
  2171  	} else {
  2172  		// But if we are using bind-mounts, we can just copy it to where the snapshot is
  2173  		// mounted into the db container (/mnt/ddev_config/db_snapshots)
  2174  		c := fmt.Sprintf("cp -r %s/%s /mnt/ddev_config/db_snapshots", containerSnapshotDir, snapshotFile)
  2175  		uid, _, _ := util.GetContainerUIDGid()
  2176  		if app.Database.Type == nodeps.Postgres {
  2177  			uid = "999"
  2178  		}
  2179  		stdout, stderr, err = dockerutil.Exec(dbContainer.ID, c, uid)
  2180  		if err != nil {
  2181  			return "", fmt.Errorf("failed to '%s': %v, stdout=%s, stderr=%s", c, err, stdout, stderr)
  2182  		}
  2183  	}
  2184  
  2185  	// Clean up the in-container dir that we just used
  2186  	_, _, err = dockerutil.Exec(dbContainer.ID, fmt.Sprintf("rm -f %s/%s", containerSnapshotDir, snapshotFile), "")
  2187  	if err != nil {
  2188  		return "", err
  2189  	}
  2190  	err = app.ProcessHooks("post-snapshot")
  2191  	if err != nil {
  2192  		return snapshotFile, fmt.Errorf("failed to process pre-stop hooks: %v", err)
  2193  	}
  2194  
  2195  	return snapshotName, nil
  2196  }
  2197  
  2198  // getBackupCommand returns the command to dump the entire db system for the various databases
  2199  func getBackupCommand(app *DdevApp, targetFile string) string {
  2200  
  2201  	c := fmt.Sprintf(`mariabackup --backup --stream=mbstream --user=root --password=root --socket=/var/tmp/mysql.sock  2>/tmp/snapshot_%s.log | gzip > "%s"`, path.Base(targetFile), targetFile)
  2202  
  2203  	oldMariaVersions := []string{"5.5", "10.0"}
  2204  
  2205  	switch {
  2206  	// Old mariadb versions don't have mariabackup, use xtrabackup for them as well as MySQL
  2207  	case app.Database.Type == nodeps.MariaDB && nodeps.ArrayContainsString(oldMariaVersions, app.Database.Version):
  2208  		fallthrough
  2209  	case app.Database.Type == nodeps.MySQL:
  2210  		c = fmt.Sprintf(`xtrabackup --backup --stream=xbstream --user=root --password=root --socket=/var/tmp/mysql.sock  2>/tmp/snapshot_%s.log | gzip > "%s"`, path.Base(targetFile), targetFile)
  2211  	case app.Database.Type == nodeps.Postgres:
  2212  		c = fmt.Sprintf("rm -rf /var/tmp/pgbackup && pg_basebackup -D /var/tmp/pgbackup 2>/tmp/snapshot_%s.log && tar -czf %s -C /var/tmp/pgbackup/ .", path.Base(targetFile), targetFile)
  2213  	}
  2214  	return c
  2215  }
  2216  
  2217  // fullDBFromVersion takes just a mariadb or mysql version number
  2218  // in x.xx format and returns something like mariadb-10.5
  2219  func fullDBFromVersion(v string) string {
  2220  	snapshotDBVersion := ""
  2221  	// The old way (when we only had mariadb and then when had mariadb and also mysql)
  2222  	// was to just have the version number and derive the database type from it,
  2223  	// so that's what is going on here. But we create a string like "mariadb_10.3" from
  2224  	// the version number
  2225  	switch {
  2226  	case v == "5.6" || v == "5.7" || v == "8.0":
  2227  		snapshotDBVersion = "mysql_" + v
  2228  
  2229  	// 5.5 isn't actually necessarily correct, because could be
  2230  	// mysql 5.5. But maria and mysql 5.5 databases were compatible anyway.
  2231  	case v == "5.5" || v >= "10.0":
  2232  		snapshotDBVersion = "mariadb_" + v
  2233  	}
  2234  	return snapshotDBVersion
  2235  }
  2236  
  2237  // Stop stops and Removes the docker containers for the project in current directory.
  2238  func (app *DdevApp) Stop(removeData bool, createSnapshot bool) error {
  2239  	app.DockerEnv()
  2240  	var err error
  2241  
  2242  	if app.Name == "" {
  2243  		return fmt.Errorf("invalid app.Name provided to app.Stop(), app=%v", app)
  2244  	}
  2245  	err = app.ProcessHooks("pre-stop")
  2246  	if err != nil {
  2247  		return fmt.Errorf("failed to process pre-stop hooks: %v", err)
  2248  	}
  2249  	status, _ := app.SiteStatus()
  2250  
  2251  	if createSnapshot == true {
  2252  		if status != SiteRunning {
  2253  			util.Warning("Must start non-running project to do database snapshot")
  2254  			err = app.Start()
  2255  			if err != nil {
  2256  				return fmt.Errorf("failed to start project to perform database snapshot")
  2257  			}
  2258  		}
  2259  		t := time.Now()
  2260  		_, err = app.Snapshot(app.Name + "_remove_data_snapshot_" + t.Format("20060102150405"))
  2261  		if err != nil {
  2262  			return err
  2263  		}
  2264  	}
  2265  
  2266  	err = SyncAndPauseMutagenSession(app)
  2267  	if err != nil {
  2268  		util.Warning("Unable to SyncAndterminateMutagenSession: %v", err)
  2269  	}
  2270  
  2271  	if globalconfig.DdevGlobalConfig.UseTraefik && status == SiteRunning {
  2272  		_, _, err = app.Exec(&ExecOpts{
  2273  			Cmd: fmt.Sprintf("rm -f /mnt/ddev-global-cache/traefik/*/%s.{yaml,crt,key}", app.Name),
  2274  		})
  2275  		if err != nil {
  2276  			util.Warning("Unable to clean up traefik configuration: %v", err)
  2277  		}
  2278  	}
  2279  	// If project is running, clean up ddev-global-cache
  2280  	if status == SiteRunning && removeData {
  2281  		_, _, err = app.Exec(&ExecOpts{
  2282  			Cmd: fmt.Sprintf("rm -rf /mnt/ddev-global-cache/*/%s* /mnt/ddev-global-cache/traefik/*/%s*", app.Name, app.Name),
  2283  		})
  2284  		if err != nil {
  2285  			util.Warning("Unable to clean up ddev-global-cache: %v", err)
  2286  		}
  2287  	}
  2288  
  2289  	if status == SiteRunning {
  2290  		err = app.Pause()
  2291  		if err != nil {
  2292  			util.Warning("Failed to pause containers for %s: %v", app.GetName(), err)
  2293  		}
  2294  	}
  2295  	// Remove all the containers and volumes for app.
  2296  	err = Cleanup(app)
  2297  	if err != nil {
  2298  		return err
  2299  	}
  2300  
  2301  	// Remove data/database/projectInfo/hosts entry if we need to.
  2302  	if removeData {
  2303  		err = TerminateMutagenSync(app)
  2304  		if err != nil {
  2305  			util.Warning("unable to terminate mutagen session %s: %v", MutagenSyncName(app.Name), err)
  2306  		}
  2307  		if err = app.RemoveHostsEntriesIfNeeded(); err != nil {
  2308  			return fmt.Errorf("failed to remove hosts entries: %v", err)
  2309  		}
  2310  		app.RemoveGlobalProjectInfo()
  2311  		err = globalconfig.WriteGlobalConfig(globalconfig.DdevGlobalConfig)
  2312  		if err != nil {
  2313  			util.Warning("could not WriteGlobalConfig: %v", err)
  2314  		}
  2315  
  2316  		vols := []string{app.GetMariaDBVolumeName(), app.GetPostgresVolumeName(), GetMutagenVolumeName(app)}
  2317  		if globalconfig.DdevGlobalConfig.NoBindMounts {
  2318  			vols = append(vols, app.Name+"-ddev-config")
  2319  		}
  2320  		for _, volName := range vols {
  2321  			err = dockerutil.RemoveVolume(volName)
  2322  			if err != nil {
  2323  				util.Warning("could not remove volume %s: %v", volName, err)
  2324  			} else {
  2325  				util.Success("Volume %s for project %s was deleted", volName, app.Name)
  2326  			}
  2327  		}
  2328  		deleteServiceVolumes(app)
  2329  
  2330  		dbBuilt := app.GetDBImage() + "-" + app.Name + "-built"
  2331  		_ = dockerutil.RemoveImage(dbBuilt)
  2332  
  2333  		webBuilt := versionconstants.GetWebImage() + "-" + app.Name + "-built"
  2334  		_ = dockerutil.RemoveImage(webBuilt)
  2335  		util.Success("Project %s was deleted. Your code and configuration are unchanged.", app.Name)
  2336  	}
  2337  
  2338  	err = app.ProcessHooks("post-stop")
  2339  	if err != nil {
  2340  		return fmt.Errorf("failed to process post-stop hooks: %v", err)
  2341  	}
  2342  
  2343  	return nil
  2344  }
  2345  
  2346  // deleteServiceVolumes finds all the volumes created by services and removes them.
  2347  // All volumes that are not external (likely not global) are removed.
  2348  func deleteServiceVolumes(app *DdevApp) {
  2349  	var err error
  2350  	y := app.ComposeYaml
  2351  	if s, ok := y["volumes"]; ok {
  2352  		for _, v := range s.(map[string]interface{}) {
  2353  			vol := v.(map[string]interface{})
  2354  			if vol["external"] == true {
  2355  				continue
  2356  			}
  2357  			if vol["name"] == nil {
  2358  				continue
  2359  			}
  2360  			volName := vol["name"].(string)
  2361  
  2362  			if dockerutil.VolumeExists(volName) {
  2363  				err = dockerutil.RemoveVolume(volName)
  2364  				if err != nil {
  2365  					util.Warning("could not remove volume %s: %v", volName, err)
  2366  				} else {
  2367  					util.Success("Deleting third-party persistent volume %s", volName)
  2368  				}
  2369  			}
  2370  		}
  2371  	}
  2372  }
  2373  
  2374  // RemoveGlobalProjectInfo deletes the project from ProjectList
  2375  func (app *DdevApp) RemoveGlobalProjectInfo() {
  2376  	_ = globalconfig.RemoveProjectInfo(app.Name)
  2377  }
  2378  
  2379  // GetHTTPURL returns the HTTP URL for an app.
  2380  func (app *DdevApp) GetHTTPURL() string {
  2381  	url := ""
  2382  	if !IsRouterDisabled(app) {
  2383  		url = "http://" + app.GetHostname()
  2384  		if app.RouterHTTPPort != "80" {
  2385  			url = url + ":" + app.RouterHTTPPort
  2386  		}
  2387  	} else {
  2388  		url = app.GetWebContainerDirectHTTPURL()
  2389  	}
  2390  	return url
  2391  }
  2392  
  2393  // GetHTTPSURL returns the HTTPS URL for an app.
  2394  func (app *DdevApp) GetHTTPSURL() string {
  2395  	url := ""
  2396  	if !IsRouterDisabled(app) {
  2397  		url = "https://" + app.GetHostname()
  2398  		if app.RouterHTTPSPort != "443" {
  2399  			url = url + ":" + app.RouterHTTPSPort
  2400  		}
  2401  	} else {
  2402  		url = app.GetWebContainerDirectHTTPSURL()
  2403  	}
  2404  	return url
  2405  }
  2406  
  2407  // GetAllURLs returns an array of all the URLs for the project
  2408  func (app *DdevApp) GetAllURLs() (httpURLs []string, httpsURLs []string, allURLs []string) {
  2409  	if nodeps.IsGitpod() {
  2410  		url, err := exec.RunHostCommand("gp", "url", app.HostWebserverPort)
  2411  		if err == nil {
  2412  			url = strings.Trim(url, "\n")
  2413  			httpsURLs = append(httpsURLs, url)
  2414  		}
  2415  	}
  2416  	if nodeps.IsCodespaces() {
  2417  		codespaceName := os.Getenv("CODESPACE_NAME")
  2418  		if codespaceName != "" {
  2419  			url := fmt.Sprintf("https://%s-%s.preview.app.github.dev", codespaceName, app.HostWebserverPort)
  2420  			httpsURLs = append(httpsURLs, url)
  2421  		}
  2422  	}
  2423  
  2424  	// Get configured URLs
  2425  	for _, name := range app.GetHostnames() {
  2426  		httpPort := ""
  2427  		httpsPort := ""
  2428  		if app.RouterHTTPPort != "80" {
  2429  			httpPort = ":" + app.RouterHTTPPort
  2430  		}
  2431  		if app.RouterHTTPSPort != "443" {
  2432  			httpsPort = ":" + app.RouterHTTPSPort
  2433  		}
  2434  
  2435  		httpsURLs = append(httpsURLs, "https://"+name+httpsPort)
  2436  		httpURLs = append(httpURLs, "http://"+name+httpPort)
  2437  	}
  2438  
  2439  	if !IsRouterDisabled(app) {
  2440  		httpsURLs = append(httpsURLs, app.GetWebContainerDirectHTTPSURL())
  2441  	}
  2442  	httpURLs = append(httpURLs, app.GetWebContainerDirectHTTPURL())
  2443  
  2444  	allURLs = append(httpsURLs, httpURLs...)
  2445  	return httpURLs, httpsURLs, allURLs
  2446  }
  2447  
  2448  // GetPrimaryURL returns the primary URL that can be used, https or http
  2449  func (app *DdevApp) GetPrimaryURL() string {
  2450  	httpURLs, httpsURLs, _ := app.GetAllURLs()
  2451  	urlList := httpsURLs
  2452  	// If no mkcert trusted https, use the httpURLs instead
  2453  	if !nodeps.IsGitpod() && !nodeps.IsCodespaces() && (globalconfig.GetCAROOT() == "" || IsRouterDisabled(app)) {
  2454  		urlList = httpURLs
  2455  	}
  2456  	if len(urlList) > 0 {
  2457  		return urlList[0]
  2458  	}
  2459  	// Failure mode, just return empty string
  2460  	return ""
  2461  }
  2462  
  2463  // GetWebContainerDirectHTTPURL returns the URL that can be used without the router to get to web container.
  2464  func (app *DdevApp) GetWebContainerDirectHTTPURL() string {
  2465  	// Get direct address of web container
  2466  	dockerIP, err := dockerutil.GetDockerIP()
  2467  	if err != nil {
  2468  		util.Warning("Unable to get Docker IP: %v", err)
  2469  	}
  2470  	port, _ := app.GetWebContainerPublicPort()
  2471  	return fmt.Sprintf("http://%s:%d", dockerIP, port)
  2472  }
  2473  
  2474  // GetWebContainerDirectHTTPSURL returns the URL that can be used without the router to get to web container via https.
  2475  func (app *DdevApp) GetWebContainerDirectHTTPSURL() string {
  2476  	// Get direct address of web container
  2477  	dockerIP, err := dockerutil.GetDockerIP()
  2478  	if err != nil {
  2479  		util.Warning("Unable to get Docker IP: %v", err)
  2480  	}
  2481  	port, _ := app.GetWebContainerHTTPSPublicPort()
  2482  	return fmt.Sprintf("https://%s:%d", dockerIP, port)
  2483  }
  2484  
  2485  // GetWebContainerPublicPort returns the direct-access public tcp port for http
  2486  func (app *DdevApp) GetWebContainerPublicPort() (int, error) {
  2487  
  2488  	webContainer, err := app.FindContainerByType("web")
  2489  	if err != nil || webContainer == nil {
  2490  		return -1, fmt.Errorf("unable to find web container for app: %s, err %v", app.Name, err)
  2491  	}
  2492  
  2493  	for _, p := range webContainer.Ports {
  2494  		if p.PrivatePort == 80 {
  2495  			return int(p.PublicPort), nil
  2496  		}
  2497  	}
  2498  	return -1, fmt.Errorf("no public port found for private port 80")
  2499  }
  2500  
  2501  // GetWebContainerHTTPSPublicPort returns the direct-access public tcp port for https
  2502  func (app *DdevApp) GetWebContainerHTTPSPublicPort() (int, error) {
  2503  
  2504  	webContainer, err := app.FindContainerByType("web")
  2505  	if err != nil || webContainer == nil {
  2506  		return -1, fmt.Errorf("unable to find https web container for app: %s, err %v", app.Name, err)
  2507  	}
  2508  
  2509  	for _, p := range webContainer.Ports {
  2510  		if p.PrivatePort == 443 {
  2511  			return int(p.PublicPort), nil
  2512  		}
  2513  	}
  2514  	return -1, fmt.Errorf("no public https port found for private port 443")
  2515  }
  2516  
  2517  // HostName returns the hostname of a given application.
  2518  func (app *DdevApp) HostName() string {
  2519  	return app.GetHostname()
  2520  }
  2521  
  2522  // GetActiveAppRoot returns the fully rooted directory of the active app, or an error
  2523  func GetActiveAppRoot(siteName string) (string, error) {
  2524  	var siteDir string
  2525  	var err error
  2526  
  2527  	if siteName == "" {
  2528  		siteDir, err = os.Getwd()
  2529  		if err != nil {
  2530  			return "", fmt.Errorf("error determining the current directory: %s", err)
  2531  		}
  2532  		_, err = CheckForConf(siteDir)
  2533  		if err != nil {
  2534  			return "", fmt.Errorf("could not find a project in %s. Have you run 'ddev config'? Please specify a project name or change directories: %s", siteDir, err)
  2535  		}
  2536  		// Handle the case where it's registered globally but stopped
  2537  	} else if p := globalconfig.GetProject(siteName); p != nil {
  2538  		return p.AppRoot, nil
  2539  		// Or find it by looking at docker containers
  2540  	} else {
  2541  		var ok bool
  2542  
  2543  		labels := map[string]string{
  2544  			"com.ddev.site-name":         siteName,
  2545  			"com.docker.compose.service": "web",
  2546  		}
  2547  
  2548  		webContainer, err := dockerutil.FindContainerByLabels(labels)
  2549  		if err != nil {
  2550  			return "", err
  2551  		}
  2552  		if webContainer == nil {
  2553  			return "", fmt.Errorf("could not find a project named '%s'. Run 'ddev list' to see currently active projects", siteName)
  2554  		}
  2555  
  2556  		siteDir, ok = webContainer.Labels["com.ddev.approot"]
  2557  		if !ok {
  2558  			return "", fmt.Errorf("could not determine the location of %s from container: %s", siteName, dockerutil.ContainerName(*webContainer))
  2559  		}
  2560  	}
  2561  	appRoot, err := CheckForConf(siteDir)
  2562  	if err != nil {
  2563  		return siteDir, err
  2564  	}
  2565  
  2566  	return appRoot, nil
  2567  }
  2568  
  2569  // GetActiveApp returns the active App based on the current working directory or running siteName provided.
  2570  // To use the current working directory, siteName should be ""
  2571  func GetActiveApp(siteName string) (*DdevApp, error) {
  2572  	app := &DdevApp{}
  2573  	activeAppRoot, err := GetActiveAppRoot(siteName)
  2574  	if err != nil {
  2575  		return app, err
  2576  	}
  2577  
  2578  	// Mostly ignore app.Init() error, since app.Init() fails if no directory found. Some errors should be handled though.
  2579  	// We already were successful with *finding* the app, and if we get an
  2580  	// incomplete one we have to add to it.
  2581  	if err = app.Init(activeAppRoot); err != nil {
  2582  		switch err.(type) {
  2583  		case webContainerExists, invalidConfigFile, invalidHostname, invalidAppType, invalidPHPVersion, invalidWebserverType, invalidProvider:
  2584  			return app, err
  2585  		}
  2586  	}
  2587  
  2588  	if app.Name == "" {
  2589  		err = restoreApp(app, siteName)
  2590  		if err != nil {
  2591  			return app, err
  2592  		}
  2593  	}
  2594  
  2595  	return app, nil
  2596  }
  2597  
  2598  // restoreApp recreates an AppConfig's Name and returns an error
  2599  // if it cannot restore them.
  2600  func restoreApp(app *DdevApp, siteName string) error {
  2601  	if siteName == "" {
  2602  		return fmt.Errorf("error restoring AppConfig: no project name given")
  2603  	}
  2604  	app.Name = siteName
  2605  	return nil
  2606  }
  2607  
  2608  // GetProvider returns a pointer to the provider instance interface.
  2609  func (app *DdevApp) GetProvider(providerName string) (*Provider, error) {
  2610  
  2611  	var p Provider
  2612  	var err error
  2613  
  2614  	if providerName != "" && providerName != nodeps.ProviderDefault {
  2615  		p = Provider{
  2616  			ProviderType: providerName,
  2617  			app:          app,
  2618  		}
  2619  		err = p.Init(providerName, app)
  2620  	}
  2621  
  2622  	app.ProviderInstance = &p
  2623  	return app.ProviderInstance, err
  2624  }
  2625  
  2626  // GetWorkingDir will determine the appropriate working directory for an Exec/ExecWithTty command
  2627  // by consulting with the project configuration. If no dir is specified for the service, an
  2628  // empty string will be returned.
  2629  func (app *DdevApp) GetWorkingDir(service string, dir string) string {
  2630  	// Highest preference is for directories passed into the command directly
  2631  	if dir != "" {
  2632  		return dir
  2633  	}
  2634  
  2635  	// The next highest preference is for directories defined in config.yaml
  2636  	if app.WorkingDir != nil {
  2637  		if workingDir := app.WorkingDir[service]; workingDir != "" {
  2638  			return workingDir
  2639  		}
  2640  	}
  2641  
  2642  	// The next highest preference is for app type defaults
  2643  	return app.DefaultWorkingDirMap()[service]
  2644  }
  2645  
  2646  // GetNFSMountVolumeName returns the docker volume name of the nfs mount volume
  2647  func (app *DdevApp) GetNFSMountVolumeName() string {
  2648  	// This is lowercased because the automatic naming in docker-compose v1/2
  2649  	// defaulted to lowercase the name
  2650  	// Although some volume names are auto-lowercased by docker, this one
  2651  	// is explicitly specified by us and is not lowercased.
  2652  	return "ddev-" + app.Name + "_nfsmount"
  2653  }
  2654  
  2655  // GetMariaDBVolumeName returns the docker volume name of the mariadb/database volume
  2656  // For historical reasons this isn't lowercased.
  2657  func (app *DdevApp) GetMariaDBVolumeName() string {
  2658  	return app.Name + "-mariadb"
  2659  }
  2660  
  2661  // GetPostgresVolumeName returns the docker volume name of the Postgres/database volume
  2662  // For historical reasons this isn't lowercased.
  2663  func (app *DdevApp) GetPostgresVolumeName() string {
  2664  	return app.Name + "-postgres"
  2665  }
  2666  
  2667  // StartAppIfNotRunning is intended to replace much-duplicated code in the commands.
  2668  func (app *DdevApp) StartAppIfNotRunning() error {
  2669  	var err error
  2670  	status, _ := app.SiteStatus()
  2671  	if status != SiteRunning {
  2672  		err = app.Start()
  2673  	}
  2674  
  2675  	return err
  2676  }
  2677  
  2678  // CheckAddonIncompatibilities looks for problems with docker-compose.*.yaml 3rd-party services
  2679  func (app *DdevApp) CheckAddonIncompatibilities() error {
  2680  	if _, ok := app.ComposeYaml["services"]; !ok {
  2681  		util.Warning("Unable to check 3rd-party services for missing networks stanza")
  2682  		return nil
  2683  	}
  2684  	// Look for missing "networks" stanza and request it.
  2685  	for s, v := range app.ComposeYaml["services"].(map[string]interface{}) {
  2686  		errMsg := fmt.Errorf("service '%s' does not have the 'networks: [default, ddev_default]' stanza, required since v1.19, please add it, see %s", s, "https://ddev.readthedocs.io/en/latest/users/extend/custom-compose-files/#docker-composeyaml-examples")
  2687  		var nets map[string]interface{}
  2688  		x := v.(map[string]interface{})
  2689  		ok := false
  2690  		if nets, ok = x["networks"].(map[string]interface{}); !ok {
  2691  			return errMsg
  2692  		}
  2693  		// Make sure both "default" and "ddev" networks are in there.
  2694  		for _, requiredNetwork := range []string{"default", "ddev_default"} {
  2695  			if _, ok := nets[requiredNetwork]; !ok {
  2696  				return errMsg
  2697  			}
  2698  		}
  2699  	}
  2700  	return nil
  2701  }
  2702  
  2703  // UpdateComposeYaml updates app.ComposeYaml from available content
  2704  func (app *DdevApp) UpdateComposeYaml(content string) error {
  2705  	err := yaml.Unmarshal([]byte(content), &app.ComposeYaml)
  2706  	if err != nil {
  2707  		return err
  2708  	}
  2709  	return nil
  2710  }
  2711  
  2712  // GetContainerName returns the contructed container name of the
  2713  // service provided.
  2714  func GetContainerName(app *DdevApp, service string) string {
  2715  	return "ddev-" + app.Name + "-" + service
  2716  }
  2717  
  2718  // GetContainer returns the containerID of the app service name provided.
  2719  func GetContainer(app *DdevApp, service string) (*docker.APIContainers, error) {
  2720  	name := GetContainerName(app, service)
  2721  	cid, err := dockerutil.FindContainerByName(name)
  2722  	if err != nil || cid == nil {
  2723  		return nil, fmt.Errorf("unable to find container %s: %v", name, err)
  2724  	}
  2725  	return cid, nil
  2726  }
  2727  
  2728  // FormatSiteStatus formats "paused" or "running" with color
  2729  func FormatSiteStatus(status string) string {
  2730  	if status == SiteRunning {
  2731  		status = "OK"
  2732  	}
  2733  	formattedStatus := status
  2734  
  2735  	switch {
  2736  	case strings.Contains(status, SitePaused):
  2737  		formattedStatus = util.ColorizeText(formattedStatus, "yellow")
  2738  	case strings.Contains(status, SiteStopped) || strings.Contains(status, SiteDirMissing) || strings.Contains(status, SiteConfigMissing):
  2739  		formattedStatus = util.ColorizeText(formattedStatus, "red")
  2740  	default:
  2741  		formattedStatus = util.ColorizeText(formattedStatus, "green")
  2742  	}
  2743  	return formattedStatus
  2744  }
  2745  
  2746  // GetHostUploadDirFullPath returns the full path to the upload directory on the host or "" if there is none
  2747  func (app *DdevApp) GetHostUploadDirFullPath() string {
  2748  	if d := app.GetUploadDir(); d != "" {
  2749  		return path.Join(app.AppRoot, app.Docroot, d)
  2750  	}
  2751  	return ""
  2752  }
  2753  
  2754  // GetContainerUploadDirFullPath returns the full path to the upload directory in container or "" if there is none
  2755  func (app *DdevApp) GetContainerUploadDirFullPath() string {
  2756  	if d := app.GetUploadDir(); d != "" {
  2757  		return path.Join("/var/www/html", app.Docroot, d)
  2758  	}
  2759  	return ""
  2760  }
  2761  
  2762  // CreateUploadDirIfNecessary creates the upload dir if it doesn't exist, so we can properly
  2763  // set up bind-mounts when doing mutagen.
  2764  // There is no need to do it if mutagen is not enabled, and
  2765  // we'll just respect a symlink if it exists, and the user has to figure out the right
  2766  // thing to do wrt mutagen
  2767  func (app *DdevApp) CreateUploadDirIfNecessary() {
  2768  	if d := app.GetHostUploadDirFullPath(); d != "" && app.IsMutagenEnabled() && !fileutil.FileExists(d) {
  2769  		err := os.MkdirAll(app.GetHostUploadDirFullPath(), 0755)
  2770  		if err != nil {
  2771  			util.Warning("Unable to create upload directory %s: %v", app.GetHostUploadDirFullPath(), err)
  2772  		}
  2773  	}
  2774  }