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