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 }