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