github.com/drud/ddev@v1.21.5-alpha1.0.20230226034409-94fcc4b94453/pkg/ddevapp/config.go (about) 1 package ddevapp 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "path/filepath" 8 "sort" 9 "strings" 10 "text/template" 11 "time" 12 13 "github.com/Masterminds/sprig/v3" 14 "github.com/drud/ddev/pkg/dockerutil" 15 "github.com/drud/ddev/pkg/globalconfig" 16 "github.com/drud/ddev/pkg/nodeps" 17 "github.com/drud/ddev/pkg/versionconstants" 18 copy2 "github.com/otiai10/copy" 19 20 "regexp" 21 22 "runtime" 23 24 "github.com/drud/ddev/pkg/fileutil" 25 "github.com/drud/ddev/pkg/output" 26 "github.com/drud/ddev/pkg/util" 27 log "github.com/sirupsen/logrus" 28 "gopkg.in/yaml.v3" 29 ) 30 31 // Regexp pattern to determine if a hostname is valid per RFC 1123. 32 var hostRegex = regexp.MustCompile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`) 33 34 // init() is for testing situations only, allowing us to override the default webserver type 35 // or caching behavior 36 37 func init() { 38 // This is for automated testing only. It allows us to override the webserver type. 39 if testWebServerType := os.Getenv("DDEV_TEST_WEBSERVER_TYPE"); testWebServerType != "" { 40 nodeps.WebserverDefault = testWebServerType 41 } 42 if testNFSMount := os.Getenv("DDEV_TEST_USE_NFSMOUNT"); testNFSMount != "" { 43 nodeps.NFSMountEnabledDefault = true 44 } 45 if testMutagen := os.Getenv("DDEV_TEST_USE_MUTAGEN"); testMutagen == "true" { 46 nodeps.MutagenEnabledDefault = true 47 } 48 if os.Getenv("DDEV_TEST_NO_BIND_MOUNTS") == "true" { 49 nodeps.NoBindMountsDefault = true 50 } 51 if os.Getenv("DDEV_TEST_USE_TRAEFIK") == "true" { 52 nodeps.UseTraefikDefault = true 53 } 54 55 } 56 57 // NewApp creates a new DdevApp struct with defaults set and overridden by any existing config.yml. 58 func NewApp(appRoot string, includeOverrides bool) (*DdevApp, error) { 59 runTime := util.TimeTrack(time.Now(), fmt.Sprintf("ddevapp.NewApp(%s)", appRoot)) 60 defer runTime() 61 62 app := &DdevApp{} 63 64 if appRoot == "" { 65 app.AppRoot, _ = os.Getwd() 66 } else { 67 app.AppRoot = appRoot 68 } 69 70 homeDir, _ := os.UserHomeDir() 71 if appRoot == filepath.Dir(globalconfig.GetGlobalDdevDir()) || app.AppRoot == homeDir { 72 return nil, fmt.Errorf("ddev config is not useful in your home directory (%s)", homeDir) 73 } 74 75 if !fileutil.FileExists(app.AppRoot) { 76 return app, fmt.Errorf("project root %s does not exist", app.AppRoot) 77 } 78 app.ConfigPath = app.GetConfigPath("config.yaml") 79 app.Type = nodeps.AppTypePHP 80 app.PHPVersion = nodeps.PHPDefault 81 app.ComposerVersion = nodeps.ComposerDefault 82 app.NodeJSVersion = nodeps.NodeJSDefault 83 app.WebserverType = nodeps.WebserverDefault 84 app.NFSMountEnabled = nodeps.NFSMountEnabledDefault 85 app.NFSMountEnabledGlobal = globalconfig.DdevGlobalConfig.NFSMountEnabledGlobal 86 app.MutagenEnabled = nodeps.MutagenEnabledDefault 87 app.MutagenEnabledGlobal = globalconfig.DdevGlobalConfig.MutagenEnabledGlobal 88 app.FailOnHookFail = nodeps.FailOnHookFailDefault 89 app.FailOnHookFailGlobal = globalconfig.DdevGlobalConfig.FailOnHookFailGlobal 90 app.RouterHTTPPort = nodeps.DdevDefaultRouterHTTPPort 91 app.RouterHTTPSPort = nodeps.DdevDefaultRouterHTTPSPort 92 app.PHPMyAdminPort = nodeps.DdevDefaultPHPMyAdminPort 93 app.PHPMyAdminHTTPSPort = nodeps.DdevDefaultPHPMyAdminHTTPSPort 94 app.MailhogPort = nodeps.DdevDefaultMailhogPort 95 app.MailhogHTTPSPort = nodeps.DdevDefaultMailhogHTTPSPort 96 97 // Provide a default app name based on directory name 98 app.Name = filepath.Base(app.AppRoot) 99 100 // Gather containers to omit, adding ddev-router for gitpod/codespaces 101 app.OmitContainersGlobal = globalconfig.DdevGlobalConfig.OmitContainersGlobal 102 if nodeps.IsGitpod() || nodeps.IsCodespaces() { 103 app.OmitContainersGlobal = append(app.OmitContainersGlobal, "ddev-router") 104 } 105 106 app.ProjectTLD = globalconfig.DdevGlobalConfig.ProjectTldGlobal 107 if globalconfig.DdevGlobalConfig.ProjectTldGlobal == "" { 108 app.ProjectTLD = nodeps.DdevDefaultTLD 109 } 110 app.UseDNSWhenPossible = true 111 112 app.WebImage = versionconstants.GetWebImage() 113 114 // Load from file if available. This will return an error if the file doesn't exist, 115 // and it is up to the caller to determine if that's an issue. 116 if _, err := os.Stat(app.ConfigPath); !os.IsNotExist(err) { 117 _, err = app.ReadConfig(includeOverrides) 118 if err != nil { 119 return app, fmt.Errorf("%v exists but cannot be read. It may be invalid due to a syntax error.: %v", app.ConfigPath, err) 120 } 121 } 122 123 // Upgrade any pre-v1.19.0 config that has mariadb_version or mysql_version 124 if app.MariaDBVersion != "" { 125 app.Database = DatabaseDesc{Type: nodeps.MariaDB, Version: app.MariaDBVersion} 126 app.MariaDBVersion = "" 127 } 128 if app.MySQLVersion != "" { 129 app.Database = DatabaseDesc{Type: nodeps.MySQL, Version: app.MySQLVersion} 130 app.MySQLVersion = "" 131 } 132 if app.Database.Type == "" { 133 app.Database = DatabaseDefault 134 } 135 if app.DefaultContainerTimeout == "" { 136 app.DefaultContainerTimeout = nodeps.DefaultDefaultContainerTimeout 137 } 138 139 app.SetApptypeSettingsPaths() 140 141 // Rendered yaml is not there until after ddev config or ddev start 142 if fileutil.FileExists(app.ConfigPath) && fileutil.FileExists(app.DockerComposeFullRenderedYAMLPath()) { 143 content, err := fileutil.ReadFileIntoString(app.DockerComposeFullRenderedYAMLPath()) 144 if err != nil { 145 return app, err 146 } 147 err = app.UpdateComposeYaml(content) 148 if err != nil { 149 return app, err 150 } 151 } 152 return app, nil 153 } 154 155 // GetConfigPath returns the path to an application config file specified by filename. 156 func (app *DdevApp) GetConfigPath(filename string) string { 157 return filepath.Join(app.AppRoot, ".ddev", filename) 158 } 159 160 // WriteConfig writes the app configuration into the .ddev folder. 161 func (app *DdevApp) WriteConfig() error { 162 163 // Work against a copy of the DdevApp, since we don't want to actually change it. 164 appcopy := *app 165 166 // Only set the images on write if non-default values have been specified. 167 if appcopy.WebImage == versionconstants.GetWebImage() { 168 appcopy.WebImage = "" 169 } 170 if appcopy.MailhogPort == nodeps.DdevDefaultMailhogPort { 171 appcopy.MailhogPort = "" 172 } 173 if appcopy.MailhogHTTPSPort == nodeps.DdevDefaultMailhogHTTPSPort { 174 appcopy.MailhogHTTPSPort = "" 175 } 176 if appcopy.PHPMyAdminPort == nodeps.DdevDefaultPHPMyAdminPort { 177 appcopy.PHPMyAdminPort = "" 178 } 179 if appcopy.PHPMyAdminHTTPSPort == nodeps.DdevDefaultPHPMyAdminHTTPSPort { 180 appcopy.PHPMyAdminHTTPSPort = "" 181 } 182 if appcopy.ProjectTLD == nodeps.DdevDefaultTLD || appcopy.ProjectTLD == globalconfig.DdevGlobalConfig.ProjectTldGlobal { 183 appcopy.ProjectTLD = "" 184 } 185 if appcopy.DefaultContainerTimeout == nodeps.DefaultDefaultContainerTimeout { 186 appcopy.DefaultContainerTimeout = "" 187 } 188 189 // We now want to reserve the port we're writing for HostDBPort and HostWebserverPort and so they don't 190 // accidentally get used for other projects. 191 err := app.UpdateGlobalProjectList() 192 if err != nil { 193 return err 194 } 195 196 // Don't write default working dir values to config 197 defaults := appcopy.DefaultWorkingDirMap() 198 for service, defaultWorkingDir := range defaults { 199 if app.WorkingDir[service] == defaultWorkingDir { 200 delete(appcopy.WorkingDir, service) 201 } 202 } 203 204 err = PrepDdevDirectory(&appcopy) 205 if err != nil { 206 return err 207 } 208 209 cfgbytes, err := yaml.Marshal(appcopy) 210 if err != nil { 211 return err 212 } 213 214 // Append hook information and sample hook suggestions. 215 cfgbytes = append(cfgbytes, []byte(ConfigInstructions)...) 216 cfgbytes = append(cfgbytes, appcopy.GetHookDefaultComments()...) 217 218 err = os.WriteFile(appcopy.ConfigPath, cfgbytes, 0644) 219 if err != nil { 220 return err 221 } 222 223 // Allow project-specific post-config action 224 err = appcopy.PostConfigAction() 225 if err != nil { 226 return err 227 } 228 229 // Write example Dockerfiles into build directories 230 contents := []byte(` 231 #ddev-generated 232 # You can copy this Dockerfile.example to Dockerfile to add configuration 233 # or packages or anything else to your webimage 234 # These additions will be appended last to ddev's own Dockerfile 235 RUN npm install --global forever 236 RUN echo "Built on $(date)" > /build-date.txt 237 `) 238 239 err = WriteImageDockerfile(app.GetConfigPath("web-build")+"/Dockerfile.example", contents) 240 if err != nil { 241 return err 242 } 243 contents = []byte(` 244 #ddev-generated 245 # You can copy this Dockerfile.example to Dockerfile to add configuration 246 # or packages or anything else to your dbimage 247 RUN echo "Built on $(date)" > /build-date.txt 248 `) 249 250 err = WriteImageDockerfile(app.GetConfigPath("db-build")+"/Dockerfile.example", contents) 251 if err != nil { 252 return err 253 } 254 255 return nil 256 } 257 258 // UpdateGlobalProjectList updates any information about project that 259 // is tracked in global project list: 260 // - approot 261 // - configured host ports 262 // checks that configured host ports are not already 263 // reserved by another project 264 func (app *DdevApp) UpdateGlobalProjectList() error { 265 portsToReserve := []string{} 266 if app.HostDBPort != "" { 267 portsToReserve = append(portsToReserve, app.HostDBPort) 268 } 269 if app.HostWebserverPort != "" { 270 portsToReserve = append(portsToReserve, app.HostWebserverPort) 271 } 272 if app.HostHTTPSPort != "" { 273 portsToReserve = append(portsToReserve, app.HostHTTPSPort) 274 } 275 276 if len(portsToReserve) > 0 { 277 err := globalconfig.CheckHostPortsAvailable(app.Name, portsToReserve) 278 if err != nil { 279 return err 280 } 281 } 282 err := globalconfig.ReservePorts(app.Name, portsToReserve) 283 if err != nil { 284 return err 285 } 286 err = globalconfig.SetProjectAppRoot(app.Name, app.AppRoot) 287 if err != nil { 288 return err 289 } 290 291 return nil 292 } 293 294 // ReadConfig reads project configuration from the config.yaml file 295 // It does not attempt to set default values; that's NewApp's job. 296 func (app *DdevApp) ReadConfig(includeOverrides bool) ([]string, error) { 297 298 // Load base .ddev/config.yaml - original config 299 err := app.LoadConfigYamlFile(app.ConfigPath) 300 if err != nil { 301 return []string{}, fmt.Errorf("unable to load config file %s: %v", app.ConfigPath, err) 302 } 303 304 configOverrides := []string{} 305 // Load config.*.y*ml after in glob order 306 if includeOverrides { 307 glob := filepath.Join(filepath.Dir(app.ConfigPath), "config.*.y*ml") 308 configOverrides, err = filepath.Glob(glob) 309 if err != nil { 310 return []string{}, err 311 } 312 313 for _, item := range configOverrides { 314 err = app.mergeAdditionalConfigIntoApp(item) 315 316 if err != nil { 317 return []string{}, fmt.Errorf("unable to load config file %s: %v", item, err) 318 } 319 } 320 } 321 322 return append([]string{app.ConfigPath}, configOverrides...), nil 323 } 324 325 // LoadConfigYamlFile loads one config.yaml into app, overriding what might be there. 326 func (app *DdevApp) LoadConfigYamlFile(filePath string) error { 327 source, err := os.ReadFile(filePath) 328 if err != nil { 329 return fmt.Errorf("could not find an active ddev configuration at %s have you run 'ddev config'? %v", app.ConfigPath, err) 330 } 331 332 // validate extend command keys 333 err = validateHookYAML(source) 334 if err != nil { 335 return fmt.Errorf("invalid configuration in %s: %v", app.ConfigPath, err) 336 } 337 338 // ReadConfig config values from file. 339 err = yaml.Unmarshal(source, app) 340 if err != nil { 341 return err 342 } 343 return nil 344 } 345 346 // WarnIfConfigReplace just messages user about whether config is being replaced or created 347 func (app *DdevApp) WarnIfConfigReplace() { 348 if app.ConfigExists() { 349 util.Warning("You are reconfiguring the project at %s.\nThe existing configuration will be updated and replaced.", app.AppRoot) 350 } else { 351 util.Success("Creating a new ddev project config in the current directory (%s)", app.AppRoot) 352 util.Success("Once completed, your configuration will be written to %s\n", app.ConfigPath) 353 } 354 } 355 356 // PromptForConfig goes through a set of prompts to receive user input and generate an Config struct. 357 func (app *DdevApp) PromptForConfig() error { 358 359 app.WarnIfConfigReplace() 360 361 for { 362 err := app.promptForName() 363 364 if err == nil { 365 break 366 } 367 368 output.UserOut.Printf("%v", err) 369 } 370 371 if err := app.docrootPrompt(); err != nil { 372 return err 373 } 374 375 err := app.AppTypePrompt() 376 if err != nil { 377 return err 378 } 379 380 err = app.ConfigFileOverrideAction() 381 if err != nil { 382 return err 383 } 384 385 err = app.ValidateConfig() 386 if err != nil { 387 return err 388 } 389 390 return nil 391 } 392 393 // ValidateProjectName checks to see if the project name works for a proper hostname 394 func ValidateProjectName(name string) error { 395 match := hostRegex.MatchString(name) 396 if !match { 397 return fmt.Errorf("%s is not a valid project name. Please enter a project name in your configuration that will allow for a valid hostname. See https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_hostnames for valid hostname requirements", name) 398 } 399 return nil 400 } 401 402 // ValidateConfig ensures the configuration meets ddev's requirements. 403 func (app *DdevApp) ValidateConfig() error { 404 405 // validate project name 406 if err := ValidateProjectName(app.Name); err != nil { 407 return err 408 } 409 410 // validate hostnames 411 for _, hn := range app.GetHostnames() { 412 // If they have provided "*.<hostname>" then ignore the *. part. 413 hn = strings.TrimPrefix(hn, "*.") 414 if hn == "ddev.site" { 415 return fmt.Errorf("wildcarding the full hostname or using 'ddev.site' as fqdn is not allowed because other projects would not work in that case") 416 } 417 if !hostRegex.MatchString(hn) { 418 return fmt.Errorf("invalid hostname: %s. See https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_hostnames for valid hostname requirements", hn).(invalidHostname) 419 } 420 } 421 422 // validate apptype 423 if !IsValidAppType(app.Type) { 424 return fmt.Errorf("invalid app type: %s", app.Type).(invalidAppType) 425 } 426 427 // validate PHP version 428 if !nodeps.IsValidPHPVersion(app.PHPVersion) { 429 return fmt.Errorf("unsupported PHP version: %s, ddev only supports the following versions: %v", app.PHPVersion, nodeps.GetValidPHPVersions()).(invalidPHPVersion) 430 } 431 432 // validate webserver type 433 if !nodeps.IsValidWebserverType(app.WebserverType) { 434 return fmt.Errorf("unsupported webserver type: %s, ddev (%s) only supports the following webserver types: %s", app.WebserverType, runtime.GOARCH, nodeps.GetValidWebserverTypes()).(invalidWebserverType) 435 } 436 437 if !nodeps.IsValidNodeVersion(app.NodeJSVersion) { 438 return fmt.Errorf("unsupported system Node.js version: '%s'; for the system Node.js version ddev only supports %s. However, you can use 'ddev nvm install' at runtime to use any supported version", app.NodeJSVersion, nodeps.GetValidNodeVersions()) 439 } 440 441 if !nodeps.IsValidOmitContainers(app.OmitContainers) { 442 return fmt.Errorf("unsupported omit_containers: %s, ddev (%s) only supports the following for omit_containers: %s", app.OmitContainers, runtime.GOARCH, nodeps.GetValidOmitContainers()).(InvalidOmitContainers) 443 } 444 445 if !nodeps.IsValidDatabaseVersion(app.Database.Type, app.Database.Version) { 446 return fmt.Errorf("unsupported database type/version: '%s:%s', ddev %s only supports the following database types and versions: mariadb: %v, mysql: %v, postgres: %v", app.Database.Type, app.Database.Version, runtime.GOARCH, nodeps.GetValidMariaDBVersions(), nodeps.GetValidMySQLVersions(), nodeps.GetValidPostgresVersions()) 447 } 448 449 // This check is too intensive for app.Init() and ddevapp.GetActiveApp(), slows things down dramatically 450 // If the database already exists in volume and is not of this type, then throw an error 451 //if !nodeps.ArrayContainsString(app.GetOmittedContainers(), "db") { 452 // if dbType, err := app.GetExistingDBType(); err != nil || (dbType != "" && dbType != app.Database.Type+":"+app.Database.Version) { 453 // return fmt.Errorf("Unable to configure project %s with database type %s because that 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', see docs at %s", app.Name, dbType, dbType, dbType, "https://ddev.readthedocs.io/en/latest/users/extend/database-types/") 454 // } 455 //} 456 457 // golang on windows is not able to time.LoadLocation unless 458 // go is installed... so skip validation on Windows 459 if runtime.GOOS != "windows" { 460 _, err := time.LoadLocation(app.Timezone) 461 if err != nil { 462 // golang on Windows is often not able to time.LoadLocation. 463 // It often works if go is installed and $GOROOT is set, but 464 // that's not the norm for our users. 465 return fmt.Errorf("invalid timezone %s: %v", app.Timezone, err) 466 } 467 } 468 469 //if app.Database.Type == nodeps.Postgres && (nodeps.ArrayContainsString([]string{"wordpress", "magento", "magento2"}, app.Type)) { 470 // return fmt.Errorf("project type %s does not support postgres database", app.Type) 471 //} 472 473 return nil 474 } 475 476 // DockerComposeYAMLPath returns the absolute path to where the 477 // base generated yaml file should exist for this project. 478 func (app *DdevApp) DockerComposeYAMLPath() string { 479 return app.GetConfigPath(".ddev-docker-compose-base.yaml") 480 } 481 482 // DockerComposeFullRenderedYAMLPath returns the absolute path to where the 483 // the complete generated yaml file should exist for this project. 484 func (app *DdevApp) DockerComposeFullRenderedYAMLPath() string { 485 return app.GetConfigPath(".ddev-docker-compose-full.yaml") 486 } 487 488 // GetHostname returns the primary hostname of the app. 489 func (app *DdevApp) GetHostname() string { 490 return strings.ToLower(app.Name) + "." + app.ProjectTLD 491 } 492 493 // GetHostnames returns a slice of all the configured hostnames. 494 func (app *DdevApp) GetHostnames() []string { 495 496 // Use a map to make sure that we have unique hostnames 497 // The value is useless, so just use the int 1 for assignment. 498 nameListMap := make(map[string]int) 499 nameListArray := []string{} 500 501 if !IsRouterDisabled(app) { 502 for _, name := range app.AdditionalHostnames { 503 name = strings.ToLower(name) 504 nameListMap[name+"."+app.ProjectTLD] = 1 505 } 506 507 for _, name := range app.AdditionalFQDNs { 508 name = strings.ToLower(name) 509 nameListMap[name] = 1 510 } 511 512 // Make sure the primary hostname didn't accidentally get added, it will be prepended 513 delete(nameListMap, app.GetHostname()) 514 515 // Now walk the map and extract the keys into an array. 516 for k := range nameListMap { 517 nameListArray = append(nameListArray, k) 518 } 519 sort.Strings(nameListArray) 520 // We want the primary hostname to be first in the list. 521 nameListArray = append([]string{app.GetHostname()}, nameListArray...) 522 } 523 return nameListArray 524 } 525 526 // CheckCustomConfig warns the user if any custom configuration files are in use. 527 func (app *DdevApp) CheckCustomConfig() { 528 529 // Get the path to .ddev for the current app. 530 ddevDir := filepath.Dir(app.ConfigPath) 531 532 customConfig := false 533 if _, err := os.Stat(filepath.Join(ddevDir, "nginx-site.conf")); err == nil && app.WebserverType == nodeps.WebserverNginxFPM { 534 util.Warning("Using custom nginx configuration in nginx-site.conf") 535 customConfig = true 536 } 537 nginxFullConfigPath := app.GetConfigPath("nginx_full/nginx-site.conf") 538 sigFound, _ := fileutil.FgrepStringInFile(nginxFullConfigPath, nodeps.DdevFileSignature) 539 if !sigFound && app.WebserverType == nodeps.WebserverNginxFPM { 540 util.Warning("Using custom nginx configuration in %s", nginxFullConfigPath) 541 customConfig = true 542 } 543 544 apacheFullConfigPath := app.GetConfigPath("apache/apache-site.conf") 545 sigFound, _ = fileutil.FgrepStringInFile(apacheFullConfigPath, nodeps.DdevFileSignature) 546 if !sigFound && app.WebserverType != nodeps.WebserverNginxFPM { 547 util.Warning("Using custom apache configuration in %s", apacheFullConfigPath) 548 customConfig = true 549 } 550 551 nginxPath := filepath.Join(ddevDir, "nginx") 552 if _, err := os.Stat(nginxPath); err == nil { 553 nginxFiles, err := filepath.Glob(nginxPath + "/*.conf") 554 util.CheckErr(err) 555 if len(nginxFiles) > 0 { 556 util.Warning("Using nginx snippets: %v", nginxFiles) 557 customConfig = true 558 } 559 } 560 561 mysqlPath := filepath.Join(ddevDir, "mysql") 562 if _, err := os.Stat(mysqlPath); err == nil { 563 mysqlFiles, err := filepath.Glob(mysqlPath + "/*.cnf") 564 util.CheckErr(err) 565 if len(mysqlFiles) > 0 { 566 util.Warning("Using custom mysql configuration: %v", mysqlFiles) 567 customConfig = true 568 } 569 } 570 571 phpPath := filepath.Join(ddevDir, "php") 572 if _, err := os.Stat(phpPath); err == nil { 573 phpFiles, err := filepath.Glob(phpPath + "/*.ini") 574 util.CheckErr(err) 575 if len(phpFiles) > 0 { 576 util.Warning("Using custom PHP configuration: %v", phpFiles) 577 customConfig = true 578 } 579 } 580 581 webEntrypointPath := filepath.Join(ddevDir, "web-entrypoint.d") 582 if _, err := os.Stat(webEntrypointPath); err == nil { 583 entrypointFiles, err := filepath.Glob(webEntrypointPath + "/*.sh") 584 util.CheckErr(err) 585 if len(entrypointFiles) > 0 { 586 util.Warning("Using custom web-entrypoint.d configuration: %v", entrypointFiles) 587 customConfig = true 588 } 589 } 590 591 if customConfig { 592 util.Warning("Custom configuration is updated on restart.\nIf you don't see your custom configuration taking effect, run 'ddev restart'.") 593 } 594 595 } 596 597 // CheckDeprecations warns the user if anything in use is deprecated. 598 func (app *DdevApp) CheckDeprecations() { 599 600 } 601 602 // FixObsolete removes files that may be obsolete, etc. 603 func (app *DdevApp) FixObsolete() { 604 605 // Remove old in-project commands (which have been moved to global) 606 for _, command := range []string{"db/mysql", "host/launch", "web/xdebug"} { 607 cmdPath := app.GetConfigPath(filepath.Join("commands", command)) 608 signatureFound, err := fileutil.FgrepStringInFile(cmdPath, nodeps.DdevFileSignature) 609 if err == nil && signatureFound { 610 err = os.Remove(cmdPath) 611 if err != nil { 612 util.Warning("attempted to remove %s but failed, you may want to remove it manually: %v", cmdPath, err) 613 } 614 } 615 } 616 617 // Remove old provider/*.example as we migrate to not needing them. 618 for _, providerFile := range []string{"platform.yaml.example"} { 619 providerFilePath := app.GetConfigPath(filepath.Join("providers", providerFile)) 620 err := os.Remove(providerFilePath) 621 if err == nil { 622 util.Success("Removed obsolete file %s", providerFilePath) 623 } 624 } 625 626 // Remove old global commands 627 for _, command := range []string{"host/yarn"} { 628 cmdPath := filepath.Join(globalconfig.GetGlobalDdevDir(), "commands/", command) 629 if _, err := os.Stat(cmdPath); err == nil { 630 err1 := os.Remove(cmdPath) 631 if err1 != nil { 632 util.Warning("attempted to remove %s but failed, you may want to remove it manually: %v", cmdPath, err) 633 } 634 } 635 } 636 } 637 638 type composeYAMLVars struct { 639 Name string 640 Plugin string 641 AppType string 642 MailhogPort string 643 HostMailhogPort string 644 DBType string 645 DBVersion string 646 DBMountDir string 647 DBAPort string 648 DBPort string 649 HostPHPMyAdminPort string 650 DdevGenerated string 651 HostDockerInternalIP string 652 NFSServerAddr string 653 DisableSettingsManagement bool 654 MountType string 655 WebMount string 656 WebBuildContext string 657 DBBuildContext string 658 WebBuildDockerfile string 659 DBBuildDockerfile string 660 SSHAgentBuildContext string 661 OmitDB bool 662 OmitDBA bool 663 OmitRouter bool 664 OmitSSHAgent bool 665 BindAllInterfaces bool 666 MariaDBVolumeName string 667 PostgresVolumeName string 668 MutagenEnabled bool 669 MutagenVolumeName string 670 NFSMountEnabled bool 671 NFSSource string 672 NFSMountVolumeName string 673 DockerIP string 674 IsWindowsFS bool 675 NoProjectMount bool 676 Hostnames []string 677 Timezone string 678 ComposerVersion string 679 Username string 680 UID string 681 GID string 682 AutoRestartContainers bool 683 FailOnHookFail bool 684 WebWorkingDir string 685 DBWorkingDir string 686 DBAWorkingDir string 687 WebEnvironment []string 688 NoBindMounts bool 689 Docroot string 690 ContainerUploadDir string 691 HostUploadDir string 692 GitDirMount bool 693 IsGitpod bool 694 IsCodespaces bool 695 DefaultContainerTimeout string 696 UseHostDockerInternalExtraHosts bool 697 WebExtraHTTPPorts string 698 WebExtraHTTPSPorts string 699 WebExtraExposedPorts string 700 EnvFile string 701 } 702 703 // RenderComposeYAML renders the contents of .ddev/.ddev-docker-compose*. 704 func (app *DdevApp) RenderComposeYAML() (string, error) { 705 var doc bytes.Buffer 706 var err error 707 708 hostDockerInternalIP, err := dockerutil.GetHostDockerInternalIP() 709 if err != nil { 710 util.Warning("Could not determine host.docker.internal IP address: %v", err) 711 } 712 nfsServerAddr, err := dockerutil.GetNFSServerAddr() 713 if err != nil { 714 util.Warning("Could not determine NFS server IP address: %v", err) 715 } 716 717 // The fallthrough default for hostDockerInternalIdentifier is the 718 // hostDockerInternalHostname == host.docker.internal 719 720 webEnvironment := globalconfig.DdevGlobalConfig.WebEnvironment 721 localWebEnvironment := app.WebEnvironment 722 for _, v := range localWebEnvironment { 723 // docker-compose won't accept a duplicate environment value 724 if !nodeps.ArrayContainsString(webEnvironment, v) { 725 webEnvironment = append(webEnvironment, v) 726 } 727 } 728 729 uid, gid, username := util.GetContainerUIDGid() 730 _, err = app.GetProvider("") 731 if err != nil { 732 return "", err 733 } 734 735 templateVars := composeYAMLVars{ 736 Name: app.Name, 737 Plugin: "ddev", 738 AppType: app.Type, 739 MailhogPort: GetExposedPort(app, "mailhog"), 740 HostMailhogPort: app.HostMailhogPort, 741 DBType: app.Database.Type, 742 DBVersion: app.Database.Version, 743 DBMountDir: "/var/lib/mysql", 744 DBAPort: GetExposedPort(app, "dba"), 745 DBPort: GetExposedPort(app, "db"), 746 HostPHPMyAdminPort: app.HostPHPMyAdminPort, 747 DdevGenerated: nodeps.DdevFileSignature, 748 HostDockerInternalIP: hostDockerInternalIP, 749 NFSServerAddr: nfsServerAddr, 750 DisableSettingsManagement: app.DisableSettingsManagement, 751 OmitDB: nodeps.ArrayContainsString(app.GetOmittedContainers(), nodeps.DBContainer), 752 OmitDBA: nodeps.ArrayContainsString(app.GetOmittedContainers(), nodeps.DBAContainer) || nodeps.ArrayContainsString(app.OmitContainers, nodeps.DBContainer), 753 OmitRouter: nodeps.ArrayContainsString(app.GetOmittedContainers(), globalconfig.DdevRouterContainer), 754 OmitSSHAgent: nodeps.ArrayContainsString(app.GetOmittedContainers(), "ddev-ssh-agent"), 755 BindAllInterfaces: app.BindAllInterfaces, 756 MutagenEnabled: app.IsMutagenEnabled() || globalconfig.DdevGlobalConfig.NoBindMounts, 757 758 NFSMountEnabled: app.IsNFSMountEnabled(), 759 NFSSource: "", 760 IsWindowsFS: runtime.GOOS == "windows", 761 NoProjectMount: app.NoProjectMount, 762 MountType: "bind", 763 WebMount: "../", 764 Hostnames: app.GetHostnames(), 765 Timezone: app.Timezone, 766 ComposerVersion: app.ComposerVersion, 767 Username: username, 768 UID: uid, 769 GID: gid, 770 WebBuildContext: "../", 771 DBBuildContext: "./.dbimageBuild", 772 AutoRestartContainers: globalconfig.DdevGlobalConfig.AutoRestartContainers, 773 FailOnHookFail: app.FailOnHookFail || app.FailOnHookFailGlobal, 774 WebWorkingDir: app.GetWorkingDir("web", ""), 775 DBWorkingDir: app.GetWorkingDir("db", ""), 776 DBAWorkingDir: app.GetWorkingDir("dba", ""), 777 WebEnvironment: webEnvironment, 778 MariaDBVolumeName: app.GetMariaDBVolumeName(), 779 PostgresVolumeName: app.GetPostgresVolumeName(), 780 NFSMountVolumeName: app.GetNFSMountVolumeName(), 781 NoBindMounts: globalconfig.DdevGlobalConfig.NoBindMounts, 782 Docroot: app.GetDocroot(), 783 HostUploadDir: app.GetHostUploadDirFullPath(), 784 ContainerUploadDir: app.GetContainerUploadDirFullPath(), 785 GitDirMount: false, 786 IsGitpod: nodeps.IsGitpod(), 787 IsCodespaces: nodeps.IsCodespaces(), 788 // Default max time we wait for containers to be healthy 789 DefaultContainerTimeout: app.DefaultContainerTimeout, 790 // Only use the extra_hosts technique for linux and only if not WSL2 791 // If WSL2 we have to figure out other things, see GetHostDockerInternalIP() 792 UseHostDockerInternalExtraHosts: runtime.GOOS == "linux" || (dockerutil.IsWSL2() && globalconfig.DdevGlobalConfig.XdebugIDELocation == globalconfig.XdebugIDELocationWSL2), 793 } 794 // We don't want to bind-mount git dir if it doesn't exist 795 if fileutil.IsDirectory(filepath.Join(app.AppRoot, ".git")) { 796 templateVars.GitDirMount = true 797 } 798 799 envFile := app.GetConfigPath(".env") 800 if fileutil.FileExists(envFile) { 801 templateVars.EnvFile = envFile 802 } 803 804 // And we don't want to bind-mount upload dir if it doesn't exist. 805 // templateVars.UploadDir is relative path rooted in approot. 806 if app.GetHostUploadDirFullPath() == "" || !fileutil.FileExists(app.GetHostUploadDirFullPath()) { 807 templateVars.HostUploadDir = "" 808 templateVars.ContainerUploadDir = "" 809 } 810 811 webimageExtraHTTPPorts := []string{} 812 webimageExtraHTTPSPorts := []string{} 813 exposedPorts := []int{} 814 for _, a := range app.WebExtraExposedPorts { 815 webimageExtraHTTPPorts = append(webimageExtraHTTPPorts, fmt.Sprintf("%d:%d", a.HTTPPort, a.WebContainerPort)) 816 webimageExtraHTTPSPorts = append(webimageExtraHTTPSPorts, fmt.Sprintf("%d:%d", a.HTTPSPort, a.WebContainerPort)) 817 exposedPorts = append(exposedPorts, a.WebContainerPort) 818 } 819 if len(exposedPorts) != 0 { 820 templateVars.WebExtraHTTPPorts = "," + strings.Join(webimageExtraHTTPPorts, ",") 821 templateVars.WebExtraHTTPSPorts = "," + strings.Join(webimageExtraHTTPSPorts, ",") 822 823 templateVars.WebExtraExposedPorts = "expose:\n - " 824 // Odd way to join ints into a string from https://stackoverflow.com/a/37533144/215713 825 templateVars.WebExtraExposedPorts = templateVars.WebExtraExposedPorts + strings.Trim(strings.Join(strings.Fields(fmt.Sprint(exposedPorts)), "\n - "), "[]") 826 } 827 828 if app.Database.Type == nodeps.Postgres { 829 templateVars.DBMountDir = "/var/lib/postgresql/data" 830 } 831 if app.IsNFSMountEnabled() { 832 templateVars.MountType = "volume" 833 templateVars.WebMount = "nfsmount" 834 templateVars.NFSSource = app.AppRoot 835 // Workaround for Catalina sharing nfs as /System/Volumes/Data 836 if runtime.GOOS == "darwin" && fileutil.IsDirectory(filepath.Join("/System/Volumes/Data", app.AppRoot)) { 837 templateVars.NFSSource = filepath.Join("/System/Volumes/Data", app.AppRoot) 838 } 839 if runtime.GOOS == "windows" { 840 // WinNFSD can only handle a mountpoint like /C/Users/rfay/workspace/d8git 841 // and completely chokes in C:\Users\rfay... 842 templateVars.NFSSource = dockerutil.MassageWindowsNFSMount(app.AppRoot) 843 } 844 } 845 846 if app.IsMutagenEnabled() { 847 templateVars.MutagenVolumeName = GetMutagenVolumeName(app) 848 } 849 850 // Add web and db extra dockerfile info 851 // If there is a user-provided Dockerfile, use that as the base and then add 852 // our extra stuff like usernames, etc. 853 // The db-build and web-build directories are used for context 854 // so must exist. They usually do. 855 856 for _, d := range []string{".webimageBuild", ".dbimageBuild"} { 857 err = os.MkdirAll(app.GetConfigPath(d), 0755) 858 if err != nil { 859 return "", err 860 } 861 // We must start with a clean base directory 862 err := fileutil.PurgeDirectory(app.GetConfigPath(d)) 863 if err != nil { 864 util.Warning("unable to clean up directory %s, you may want to delete it manually: %v", d, err) 865 } 866 } 867 err = os.MkdirAll(app.GetConfigPath("db-build"), 0755) 868 if err != nil { 869 return "", err 870 } 871 872 err = os.MkdirAll(app.GetConfigPath("web-build"), 0755) 873 if err != nil { 874 return "", err 875 } 876 877 extraWebContent := "\nRUN mkdir -p /home/$username && chown $username /home/$username && chmod 600 /home/$username/.pgpass" 878 extraWebContent = extraWebContent + "\nENV NVM_DIR=/home/$username/.nvm" 879 if app.NodeJSVersion != nodeps.NodeJSDefault { 880 extraWebContent = extraWebContent + "\nRUN (apt-get remove -y nodejs || true) && (apt purge nodejs || true)" 881 // Download of setup_*.sh seems to fail a LOT, probably a problem on their end. So try it twice 882 extraWebContent = extraWebContent + fmt.Sprintf("\nRUN curl -sSL --fail -o /tmp/setup_node.sh https://deb.nodesource.com/setup_%s.x || curl -sSL --fail -o /tmp/setup_node.sh https://deb.nodesource.com/setup_%s.sh >/tmp/setup_node.sh", app.NodeJSVersion, app.NodeJSVersion) 883 extraWebContent = extraWebContent + "\nRUN bash /tmp/setup_node.sh >/dev/null && apt-get install -y nodejs >/dev/null\n" + 884 "RUN npm install --unsafe-perm=true --global gulp-cli yarn || ( npm config set unsafe-perm true && npm install --global gulp-cli yarn )" 885 } 886 887 // Add supervisord config for WebExtraDaemons 888 var supervisorGroup []string 889 for _, appStart := range app.WebExtraDaemons { 890 supervisorGroup = append(supervisorGroup, appStart.Name) 891 supervisorConf := fmt.Sprintf(` 892 [program:%s] 893 group=webextradaemons 894 command=bash -c "%s || sleep 2" 895 directory=%s 896 autostart=false 897 autorestart=true 898 startretries=15 899 stdout_logfile=/proc/self/fd/2 900 stdout_logfile_maxbytes=0 901 redirect_stderr=true 902 `, appStart.Name, appStart.Command, appStart.Directory) 903 err = os.WriteFile(app.GetConfigPath(fmt.Sprintf(".webimageBuild/%s.conf", appStart.Name)), []byte(supervisorConf), 0755) 904 if err != nil { 905 return "", fmt.Errorf("failed to write .webimageBuild/%s.conf: %v", appStart.Name, err) 906 } 907 extraWebContent = extraWebContent + fmt.Sprintf("\nADD .ddev/.webimageBuild/%s.conf /etc/supervisor/conf.d\n", appStart.Name) 908 } 909 if len(supervisorGroup) > 0 { 910 err = os.WriteFile(app.GetConfigPath(".webimageBuild/webextradaemons.conf"), []byte("[group:webextradaemons]\nprograms="+strings.Join(supervisorGroup, ",")), 0755) 911 if err != nil { 912 return "", fmt.Errorf("failed to write .webimageBuild/webextradaemons.conf: %v", err) 913 } 914 extraWebContent = extraWebContent + "\nADD .ddev/.webimageBuild/webextradaemons.conf /etc/supervisor/conf.d\n" 915 } 916 917 err = WriteBuildDockerfile(app.GetConfigPath(".webimageBuild/Dockerfile"), app.GetConfigPath("web-build"), app.WebImageExtraPackages, app.ComposerVersion, extraWebContent) 918 if err != nil { 919 return "", err 920 } 921 922 // Add .pgpass to homedir on postgres 923 extraDBContent := "" 924 if app.Database.Type == nodeps.Postgres { 925 // Postgres 9/10/11 upstream images are stretch-based, out of support from Debian. 926 // Postgres 9/10 are out of support by Postgres and no new images being pushed, see 927 // https://github.com/docker-library/postgres/issues/1012 928 // However, they do have a postgres:11-bullseye, but we won't start using it yet 929 // because of awkward changes to $DBIMAGE. Postgres 11 will be EOL Nov 2023 930 if nodeps.ArrayContainsString([]string{nodeps.Postgres9, nodeps.Postgres10, nodeps.Postgres11}, app.Database.Version) { 931 extraDBContent = extraDBContent + ` 932 RUN rm -f /etc/apt/sources.list.d/pgdg.list 933 RUN apt-get update 934 RUN apt-get -y install apt-transport-https 935 RUN printf "deb http://apt-archive.postgresql.org/pub/repos/apt/ stretch-pgdg main" > /etc/apt/sources.list.d/pgdg.list 936 ` 937 } 938 extraDBContent = extraDBContent + ` 939 ENV PATH $PATH:/usr/lib/postgresql/$PG_MAJOR/bin 940 ADD postgres_healthcheck.sh / 941 RUN chmod ugo+rx /postgres_healthcheck.sh 942 RUN mkdir -p /etc/postgresql/conf.d && chmod 777 /etc/postgresql/conf.d 943 RUN echo "*:*:db:db:db" > ~postgres/.pgpass && chown postgres:postgres ~postgres/.pgpass && chmod 600 ~postgres/.pgpass && chmod 777 /var/tmp && ln -sf /mnt/ddev_config/postgres/postgresql.conf /etc/postgresql && echo "restore_command = 'true'" >> /var/lib/postgresql/recovery.conf 944 RUN printf "# TYPE DATABASE USER CIDR-ADDRESS METHOD \nhost all all 0.0.0.0/0 md5\nlocal all all trust\nhost replication db 0.0.0.0/0 trust\nhost replication all 0.0.0.0/0 trust\nlocal replication all trust\nlocal replication all peer\n" >/etc/postgresql/pg_hba.conf 945 RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y -o Dpkg::Options::="--force-confold" --no-install-recommends --no-install-suggests bzip2 less procps pv vim 946 ` 947 } 948 949 err = WriteBuildDockerfile(app.GetConfigPath(".dbimageBuild/Dockerfile"), app.GetConfigPath("db-build"), app.DBImageExtraPackages, "", extraDBContent) 950 951 // CopyEmbedAssets of postgres healthcheck has to be done after we WriteBuildDockerfile 952 // because that deletes the .dbimageBuild directory 953 if app.Database.Type == nodeps.Postgres { 954 err = fileutil.CopyEmbedAssets(bundledAssets, "healthcheck", app.GetConfigPath(".dbimageBuild")) 955 if err != nil { 956 return "", err 957 } 958 } 959 960 if err != nil { 961 return "", err 962 } 963 964 // SSH agent just needs extra to add the official related user, nothing else 965 err = WriteBuildDockerfile(filepath.Join(globalconfig.GetGlobalDdevDir(), ".sshimageBuild/Dockerfile"), "", nil, "", "") 966 if err != nil { 967 return "", err 968 } 969 970 templateVars.DockerIP, err = dockerutil.GetDockerIP() 971 if err != nil { 972 return "", err 973 } 974 if app.BindAllInterfaces { 975 templateVars.DockerIP = "0.0.0.0" 976 } 977 978 t, err := template.New("app_compose_template.yaml").Funcs(sprig.TxtFuncMap()).ParseFS(bundledAssets, "app_compose_template.yaml") 979 if err != nil { 980 return "", err 981 } 982 983 err = t.Execute(&doc, templateVars) 984 return doc.String(), err 985 } 986 987 // WriteBuildDockerfile writes a Dockerfile to be used in the 988 // docker-compose 'build' 989 // It may include the contents of .ddev/<container>-build 990 func WriteBuildDockerfile(fullpath string, userDockerfilePath string, extraPackages []string, composerVersion string, extraContent string) error { 991 992 // Start with user-built dockerfile if there is one. 993 err := os.MkdirAll(filepath.Dir(fullpath), 0755) 994 if err != nil { 995 return err 996 } 997 998 // Normal starting content is just the arg and base image 999 contents := ` 1000 #ddev-generated - Do not modify this file; your modifications will be overwritten. 1001 1002 ### DDEV-injected base Dockerfile contents 1003 ARG BASE_IMAGE 1004 FROM $BASE_IMAGE 1005 ` 1006 contents = contents + ` 1007 ARG username 1008 ARG uid 1009 ARG gid 1010 ARG DDEV_PHP_VERSION 1011 RUN (groupadd --gid $gid "$username" || groupadd "$username" || true) && (useradd -l -m -s "/bin/bash" --gid "$username" --comment '' --uid $uid "$username" || useradd -l -m -s "/bin/bash" --gid "$username" --comment '' "$username" || useradd -l -m -s "/bin/bash" --gid "$gid" --comment '' "$username" || useradd -l -m -s "/bin/bash" --comment '' $username ) 1012 ` 1013 // If there are user pre.Dockerfile* files, insert their contents 1014 if userDockerfilePath != "" { 1015 files, err := filepath.Glob(userDockerfilePath + "/pre.Dockerfile*") 1016 if err != nil { 1017 return err 1018 } 1019 1020 for _, file := range files { 1021 userContents, err := fileutil.ReadFileIntoString(file) 1022 if err != nil { 1023 return err 1024 } 1025 1026 contents = contents + "\n\n### From user Dockerfile " + file + ":\n" + userContents 1027 } 1028 } 1029 1030 if extraPackages != nil { 1031 contents = contents + ` 1032 ### DDEV-injected from webimage_extra_packages or dbimage_extra_packages 1033 1034 RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -qq install -y -o Dpkg::Options::="--force-confold" --no-install-recommends --no-install-suggests ` + strings.Join(extraPackages, " ") + "\n" 1035 } 1036 1037 // For webimage, update to latest composer. 1038 if strings.Contains(fullpath, "webimageBuild") { 1039 // Version to run composer self-update to the version 1040 var composerSelfUpdateArg string 1041 1042 // Remove leading and trailing spaces 1043 composerSelfUpdateArg = strings.TrimSpace(composerVersion) 1044 1045 // Composer v2 is default 1046 if composerSelfUpdateArg == "" { 1047 composerSelfUpdateArg = "2" 1048 } 1049 1050 // Major and minor versions have to be provided as option so add '--' prefix. 1051 // E.g. a major version can be 1 or 2, a minor version 2.2 or 2.1 etc. 1052 if strings.Count(composerVersion, ".") < 2 { 1053 composerSelfUpdateArg = "--" + composerSelfUpdateArg 1054 } 1055 1056 // Try composer self-update twice because of troubles with composer downloads 1057 // breaking testing. 1058 // First of all Composer is updated to latest stable release to ensure 1059 // new options of the self-update command can be used properly e.g. 1060 // selecting a branch instead of a major version only. 1061 contents = contents + fmt.Sprintf(` 1062 ### DDEV-injected composer update 1063 RUN export XDEBUG_MODE=off; composer self-update --stable || composer self-update --stable || true; composer self-update %s || composer self-update %s || true 1064 `, composerSelfUpdateArg, composerSelfUpdateArg) 1065 } 1066 1067 if extraContent != "" { 1068 contents = contents + fmt.Sprintf(` 1069 ### DDEV-injected extra content 1070 %s 1071 `, extraContent) 1072 } 1073 1074 // If there are user dockerfiles, appends their contents 1075 if userDockerfilePath != "" { 1076 files, err := filepath.Glob(userDockerfilePath + "/Dockerfile*") 1077 if err != nil { 1078 return err 1079 } 1080 1081 for _, file := range files { 1082 // Skip the example file 1083 if file == userDockerfilePath+"/Dockerfile.example" { 1084 continue 1085 } 1086 1087 userContents, err := fileutil.ReadFileIntoString(file) 1088 if err != nil { 1089 return err 1090 } 1091 1092 // Backward compatible fix, remove unnecessary BASE_IMAGE references 1093 re, err := regexp.Compile(`ARG BASE_IMAGE.*\n|FROM \$BASE_IMAGE.*\n`) 1094 if err != nil { 1095 return err 1096 } 1097 1098 userContents = re.ReplaceAllString(userContents, "") 1099 contents = contents + "\n\n### From user Dockerfile " + file + ":\n" + userContents 1100 } 1101 } 1102 1103 // Assets in the web-build directory copied to .webimageBuild so .webimageBuild can be "context" 1104 // This actually copies the Dockerfile, but it is then immediately overwritten by WriteImageDockerfile() 1105 if userDockerfilePath != "" { 1106 err = copy2.Copy(userDockerfilePath, filepath.Dir(fullpath)) 1107 if err != nil { 1108 return err 1109 } 1110 } 1111 return WriteImageDockerfile(fullpath, []byte(contents)) 1112 } 1113 1114 // WriteImageDockerfile writes a dockerfile at the fullpath (including the filename) 1115 func WriteImageDockerfile(fullpath string, contents []byte) error { 1116 err := os.MkdirAll(filepath.Dir(fullpath), 0755) 1117 if err != nil { 1118 return err 1119 } 1120 err = os.WriteFile(fullpath, contents, 0644) 1121 if err != nil { 1122 return err 1123 } 1124 return nil 1125 } 1126 1127 // prompt for a project name. 1128 func (app *DdevApp) promptForName() error { 1129 if app.Name == "" { 1130 dir, err := os.Getwd() 1131 // if working directory name is invalid for hostnames, we shouldn't suggest it 1132 if err == nil && hostRegex.MatchString(filepath.Base(dir)) { 1133 app.Name = filepath.Base(dir) 1134 } 1135 } 1136 1137 name := util.Prompt("Project name", app.Name) 1138 if err := ValidateProjectName(name); err != nil { 1139 return err 1140 } 1141 app.Name = name 1142 return nil 1143 } 1144 1145 // AvailableDocrootLocations returns an of default docroot locations to look for. 1146 func AvailableDocrootLocations() []string { 1147 return []string{ 1148 "_www", 1149 "docroot", 1150 "htdocs", 1151 "html", 1152 "pub", 1153 "public", 1154 "web", 1155 "web/public", 1156 "webroot", 1157 } 1158 } 1159 1160 // DiscoverDefaultDocroot returns the default docroot directory. 1161 func DiscoverDefaultDocroot(app *DdevApp) string { 1162 // Provide use the app.Docroot as the default docroot option. 1163 var defaultDocroot = app.Docroot 1164 if defaultDocroot == "" { 1165 for _, docroot := range AvailableDocrootLocations() { 1166 if _, err := os.Stat(filepath.Join(app.AppRoot, docroot)); err != nil { 1167 continue 1168 } 1169 1170 if fileutil.FileExists(filepath.Join(app.AppRoot, docroot, "index.php")) { 1171 defaultDocroot = docroot 1172 break 1173 } 1174 } 1175 } 1176 return defaultDocroot 1177 } 1178 1179 // Determine the document root. 1180 func (app *DdevApp) docrootPrompt() error { 1181 1182 // Determine the document root. 1183 util.Warning("\nThe docroot is the directory from which your site is served.\nThis is a relative path from your project root at %s", app.AppRoot) 1184 output.UserOut.Println("You may leave this value blank if your site files are in the project root") 1185 var docrootPrompt = "Docroot Location" 1186 var defaultDocroot = DiscoverDefaultDocroot(app) 1187 // If there is a default docroot, display it in the prompt. 1188 if defaultDocroot != "" { 1189 docrootPrompt = fmt.Sprintf("%s (%s)", docrootPrompt, defaultDocroot) 1190 } else if cd, _ := os.Getwd(); cd == filepath.Join(app.AppRoot, defaultDocroot) { 1191 // Preserve the case where the docroot is the current directory 1192 docrootPrompt = fmt.Sprintf("%s (current directory)", docrootPrompt) 1193 } else { 1194 // Explicitly state 'project root' when in a subdirectory 1195 docrootPrompt = fmt.Sprintf("%s (project root)", docrootPrompt) 1196 } 1197 1198 fmt.Print(docrootPrompt + ": ") 1199 app.Docroot = util.GetInput(defaultDocroot) 1200 1201 // Ensure the docroot exists. If it doesn't, prompt the user to verify they entered it correctly. 1202 fullPath := filepath.Join(app.AppRoot, app.Docroot) 1203 if _, err := os.Stat(fullPath); os.IsNotExist(err) { 1204 util.Warning("Warning: the provided docroot at %s does not currently exist.", fullPath) 1205 1206 // Ask the user for permission to create the docroot 1207 if !util.Confirm(fmt.Sprintf("Create docroot at %s?", fullPath)) { 1208 return fmt.Errorf("docroot must exist to continue configuration") 1209 } 1210 1211 if err = os.MkdirAll(fullPath, 0755); err != nil { 1212 return fmt.Errorf("unable to create docroot: %v", err) 1213 } 1214 1215 util.Success("Created docroot at %s.", fullPath) 1216 } 1217 1218 return nil 1219 } 1220 1221 // ConfigExists determines if a ddev config file exists for this application. 1222 func (app *DdevApp) ConfigExists() bool { 1223 if _, err := os.Stat(app.ConfigPath); os.IsNotExist(err) { 1224 return false 1225 } 1226 return true 1227 } 1228 1229 // AppTypePrompt handles the Type workflow. 1230 func (app *DdevApp) AppTypePrompt() error { 1231 validAppTypes := strings.Join(GetValidAppTypes(), ", ") 1232 typePrompt := fmt.Sprintf("Project Type [%s]", validAppTypes) 1233 1234 // First, see if we can auto detect what kind of site it is so we can set a sane default. 1235 1236 detectedAppType := app.DetectAppType() 1237 // If the detected detectedAppType is php, we'll ask them to confirm, 1238 // otherwise go with it. 1239 // If we found an application type just set it and inform the user. 1240 util.Success("Found a %s codebase at %s.", detectedAppType, filepath.Join(app.AppRoot, app.Docroot)) 1241 typePrompt = fmt.Sprintf("%s (%s)", typePrompt, detectedAppType) 1242 1243 fmt.Printf(typePrompt + ": ") 1244 appType := strings.ToLower(util.GetInput(detectedAppType)) 1245 1246 for !IsValidAppType(appType) { 1247 output.UserOut.Errorf("'%s' is not a valid project type. Allowed project types are: %s\n", appType, validAppTypes) 1248 1249 fmt.Printf(typePrompt + ": ") 1250 appType = strings.ToLower(util.GetInput(appType)) 1251 } 1252 app.Type = appType 1253 return nil 1254 } 1255 1256 // PrepDdevDirectory creates a .ddev directory in the current working directory 1257 func PrepDdevDirectory(app *DdevApp) error { 1258 var err error 1259 dir := app.GetConfigPath("") 1260 if _, err := os.Stat(dir); os.IsNotExist(err) { 1261 1262 log.WithFields(log.Fields{ 1263 "directory": dir, 1264 }).Debug("Config Directory does not exist, attempting to create.") 1265 1266 err = os.MkdirAll(dir, 0755) 1267 if err != nil { 1268 return err 1269 } 1270 } 1271 1272 err = os.MkdirAll(filepath.Join(dir, "web-entrypoint.d"), 0755) 1273 if err != nil { 1274 return err 1275 } 1276 1277 err = CreateGitIgnore(dir, "**/*.example", ".dbimageBuild", ".dbimageExtra", ".ddev-docker-*.yaml", ".*downloads", ".global_commands", ".homeadditions", ".importdb*", ".sshimageBuild", ".webimageBuild", ".webimageExtra", "apache/apache-site.conf", "commands/.gitattributes", "commands/db/mysql", "commands/host/launch", "commands/web/xdebug", "commands/web/live", "config.*.y*ml", "db_snapshots", "import-db", "import.yaml", "mutagen/mutagen.yml", "mutagen/.start-synced", "nginx_full/nginx-site.conf", "postgres/postgresql.conf", "providers/platform.yaml", "sequelpro.spf", fmt.Sprintf("traefik/config/%s.yaml", app.Name), fmt.Sprintf("traefik/certs/%s.crt", app.Name), fmt.Sprintf("traefik/certs/%s.key", app.Name), "xhprof/xhprof_prepend.php", "**/README.*") 1278 if err != nil { 1279 return fmt.Errorf("failed to create gitignore in %s: %v", dir, err) 1280 } 1281 1282 return nil 1283 } 1284 1285 // validateHookYAML validates command hooks and tasks defined in hooks for config.yaml 1286 func validateHookYAML(source []byte) error { 1287 validHooks := []string{ 1288 "pre-start", 1289 "post-start", 1290 "pre-import-db", 1291 "post-import-db", 1292 "pre-import-files", 1293 "post-import-files", 1294 "pre-composer", 1295 "post-composer", 1296 "pre-stop", 1297 "post-stop", 1298 "pre-config", 1299 "post-config", 1300 "pre-describe", 1301 "post-describe", 1302 "pre-exec", 1303 "post-exec", 1304 "pre-pause", 1305 "post-pause", 1306 "pre-pull", 1307 "post-pull", 1308 "pre-push", 1309 "post-push", 1310 "pre-snapshot", 1311 "post-snapshot", 1312 "pre-restore-snapshot", 1313 "post-restore-snapshot", 1314 } 1315 1316 validTasks := []string{ 1317 "exec", 1318 "exec-host", 1319 "composer", 1320 } 1321 1322 type Validate struct { 1323 Commands map[string][]map[string]interface{} `yaml:"hooks,omitempty"` 1324 } 1325 val := &Validate{} 1326 1327 err := yaml.Unmarshal(source, val) 1328 if err != nil { 1329 return err 1330 } 1331 1332 for foundHook, tasks := range val.Commands { 1333 var match bool 1334 for _, h := range validHooks { 1335 if foundHook == h { 1336 match = true 1337 } 1338 } 1339 if !match { 1340 return fmt.Errorf("invalid hook %s defined in config.yaml", foundHook) 1341 } 1342 1343 for _, foundTask := range tasks { 1344 var match bool 1345 for _, validTaskName := range validTasks { 1346 if _, ok := foundTask[validTaskName]; ok { 1347 match = true 1348 } 1349 } 1350 if !match { 1351 return fmt.Errorf("invalid task '%s' defined for hook %s in config.yaml", foundTask, foundHook) 1352 } 1353 1354 } 1355 1356 } 1357 1358 return nil 1359 } 1360 1361 // IsNFSMountEnabled determines whether NFS is enabled. 1362 // Mutagen trumps NFS, so if mutagen is enabled, NFS is not. 1363 func (app *DdevApp) IsNFSMountEnabled() bool { 1364 if !app.IsMutagenEnabled() && (app.NFSMountEnabled || app.NFSMountEnabledGlobal) { 1365 return true 1366 } 1367 return false 1368 }