github.com/ddev/ddev@v1.23.2-0.20240519125000-d824ffe36ff3/pkg/ddevapp/apptypes.go (about) 1 package ddevapp 2 3 import ( 4 "fmt" 5 "os" 6 "path" 7 "path/filepath" 8 "sort" 9 "strings" 10 11 "github.com/ddev/ddev/pkg/nodeps" 12 "github.com/ddev/ddev/pkg/util" 13 "github.com/maruel/natural" 14 "github.com/pkg/errors" 15 ) 16 17 // appTypeFuncs prototypes 18 // 19 // settingsCreator 20 type settingsCreator func(*DdevApp) (string, error) 21 22 // uploadDirs 23 type uploadDirs func(*DdevApp) []string 24 25 // hookDefaultComments should probably change its arg from string to app when 26 // config refactor is done. 27 type hookDefaultComments func() []byte 28 29 // composerCreateAllowedPaths 30 type composerCreateAllowedPaths func(app *DdevApp) ([]string, error) 31 32 // appTypeSettingsPaths 33 type appTypeSettingsPaths func(app *DdevApp) 34 35 // appTypeDetect returns true if the app is of the specified type 36 type appTypeDetect func(app *DdevApp) bool 37 38 // postImportDBAction can take actions after import (like warning user about 39 // required actions on Wordpress. 40 type postImportDBAction func(app *DdevApp) error 41 42 // configOverrideAction allows a particular apptype to override elements 43 // of the config for that apptype. Key example is drupal6 needing php56 44 type configOverrideAction func(app *DdevApp) error 45 46 // postConfigAction allows actions to take place at the end of ddev config 47 type postConfigAction func(app *DdevApp) error 48 49 // postStartAction allows actions to take place at the end of ddev start 50 type postStartAction func(app *DdevApp) error 51 52 // importFilesAction 53 type importFilesAction func(app *DdevApp, uploadDir, importPath, extractPath string) error 54 55 // defaultWorkingDirMap returns the app type's default working directory map 56 type defaultWorkingDirMap func(app *DdevApp, defaults map[string]string) map[string]string 57 58 // appTypeFuncs struct defines the functions that can be called (if populated) 59 // for a given appType. 60 type appTypeFuncs struct { 61 settingsCreator 62 uploadDirs 63 hookDefaultComments 64 composerCreateAllowedPaths 65 appTypeSettingsPaths 66 appTypeDetect 67 postImportDBAction 68 configOverrideAction 69 postConfigAction 70 postStartAction 71 importFilesAction 72 defaultWorkingDirMap 73 } 74 75 // appTypeMatrix is a static map that defines the various functions to be called 76 // for each apptype (CMS). 77 var appTypeMatrix map[string]appTypeFuncs 78 79 func init() { 80 appTypeMatrix = map[string]appTypeFuncs{ 81 nodeps.AppTypeBackdrop: { 82 settingsCreator: createBackdropSettingsFile, 83 uploadDirs: getBackdropUploadDirs, 84 hookDefaultComments: getBackdropHooks, 85 appTypeSettingsPaths: setBackdropSiteSettingsPaths, 86 appTypeDetect: isBackdropApp, 87 postImportDBAction: backdropPostImportDBAction, 88 postStartAction: backdropPostStartAction, 89 importFilesAction: backdropImportFilesAction, 90 defaultWorkingDirMap: docrootWorkingDir, 91 composerCreateAllowedPaths: getBackdropComposerCreateAllowedPaths, 92 }, 93 94 nodeps.AppTypeCakePHP: { 95 appTypeDetect: isCakephpApp, 96 configOverrideAction: cakephpConfigOverrideAction, 97 postStartAction: cakephpPostStartAction, 98 }, 99 100 nodeps.AppTypeCraftCms: { 101 importFilesAction: craftCmsImportFilesAction, 102 appTypeDetect: isCraftCmsApp, 103 configOverrideAction: craftCmsConfigOverrideAction, 104 postStartAction: craftCmsPostStartAction, 105 }, 106 107 nodeps.AppTypeDjango4: { 108 settingsCreator: django4SettingsCreator, 109 appTypeDetect: isDjango4App, 110 configOverrideAction: django4ConfigOverrideAction, 111 postConfigAction: django4PostConfigAction, 112 postStartAction: django4PostStartAction, 113 }, 114 115 nodeps.AppTypeDrupal6: { 116 settingsCreator: createDrupalSettingsPHP, 117 uploadDirs: getDrupalUploadDirs, 118 hookDefaultComments: getDrupal6Hooks, 119 appTypeSettingsPaths: setDrupalSiteSettingsPaths, 120 appTypeDetect: isDrupal6App, 121 configOverrideAction: drupal6ConfigOverrideAction, 122 postStartAction: drupal6PostStartAction, 123 importFilesAction: drupalImportFilesAction, 124 defaultWorkingDirMap: docrootWorkingDir, 125 composerCreateAllowedPaths: getDrupalComposerCreateAllowedPaths, 126 }, 127 128 nodeps.AppTypeDrupal7: { 129 settingsCreator: createDrupalSettingsPHP, 130 uploadDirs: getDrupalUploadDirs, 131 hookDefaultComments: getDrupal7Hooks, 132 appTypeSettingsPaths: setDrupalSiteSettingsPaths, 133 appTypeDetect: isDrupal7App, 134 configOverrideAction: drupal7ConfigOverrideAction, 135 postStartAction: drupal7PostStartAction, 136 importFilesAction: drupalImportFilesAction, 137 defaultWorkingDirMap: docrootWorkingDir, 138 composerCreateAllowedPaths: getDrupalComposerCreateAllowedPaths, 139 }, 140 141 nodeps.AppTypeDrupal: { 142 settingsCreator: createDrupalSettingsPHP, 143 uploadDirs: getDrupalUploadDirs, 144 hookDefaultComments: getDrupalHooks, 145 appTypeSettingsPaths: setDrupalSiteSettingsPaths, 146 appTypeDetect: isDrupalApp, 147 configOverrideAction: drupalConfigOverrideAction, 148 postStartAction: drupalPostStartAction, 149 importFilesAction: drupalImportFilesAction, 150 composerCreateAllowedPaths: getDrupalComposerCreateAllowedPaths, 151 }, 152 153 nodeps.AppTypeLaravel: { 154 appTypeDetect: isLaravelApp, 155 postStartAction: laravelPostStartAction, 156 }, 157 158 nodeps.AppTypeSilverstripe: { 159 appTypeDetect: isSilverstripeApp, 160 postStartAction: silverstripePostStartAction, 161 configOverrideAction: silverstripeConfigOverrideAction, 162 uploadDirs: getSilverstripeUploadDirs, 163 }, 164 165 nodeps.AppTypeMagento: { 166 settingsCreator: createMagentoSettingsFile, 167 uploadDirs: getMagentoUploadDirs, 168 appTypeSettingsPaths: setMagentoSiteSettingsPaths, 169 appTypeDetect: isMagentoApp, 170 importFilesAction: magentoImportFilesAction, 171 }, 172 173 nodeps.AppTypeMagento2: { 174 settingsCreator: createMagento2SettingsFile, 175 uploadDirs: getMagento2UploadDirs, 176 appTypeSettingsPaths: setMagento2SiteSettingsPaths, 177 appTypeDetect: isMagento2App, 178 configOverrideAction: magento2ConfigOverrideAction, 179 importFilesAction: magentoImportFilesAction, 180 }, 181 182 nodeps.AppTypePHP: { 183 postStartAction: nil, 184 }, 185 186 nodeps.AppTypePython: { 187 appTypeDetect: isPythonApp, 188 configOverrideAction: pythonConfigOverrideAction, 189 postConfigAction: pythonPostConfigAction, 190 }, 191 192 nodeps.AppTypeShopware6: { 193 appTypeDetect: isShopware6App, 194 appTypeSettingsPaths: setShopware6SiteSettingsPaths, 195 uploadDirs: getShopwareUploadDirs, 196 postStartAction: shopware6PostStartAction, 197 importFilesAction: shopware6ImportFilesAction, 198 }, 199 200 nodeps.AppTypeTYPO3: { 201 settingsCreator: createTypo3SettingsFile, 202 uploadDirs: getTypo3UploadDirs, 203 hookDefaultComments: getTypo3Hooks, 204 appTypeSettingsPaths: setTypo3SiteSettingsPaths, 205 appTypeDetect: isTypo3App, 206 importFilesAction: typo3ImportFilesAction, 207 }, 208 209 nodeps.AppTypeWordPress: { 210 settingsCreator: createWordpressSettingsFile, 211 uploadDirs: getWordpressUploadDirs, 212 hookDefaultComments: getWordpressHooks, 213 appTypeSettingsPaths: setWordpressSiteSettingsPaths, 214 appTypeDetect: isWordpressApp, 215 importFilesAction: wordpressImportFilesAction, 216 }, 217 } 218 219 drupalAlias := appTypeMatrix[nodeps.AppTypeDrupal] 220 drupalAlias.appTypeDetect = nil 221 for _, alias := range []string{nodeps.AppTypeDrupal8, nodeps.AppTypeDrupal9, nodeps.AppTypeDrupal10} { 222 appTypeMatrix[alias] = drupalAlias 223 } 224 } 225 226 // CreateSettingsFile creates the settings file (like settings.php) for the 227 // provided app is the apptype has a settingsCreator function. 228 // It also preps the ddev directory, including setting up the .ddev gitignore 229 func (app *DdevApp) CreateSettingsFile() (string, error) { 230 err := PrepDdevDirectory(app) 231 if err != nil { 232 util.Warning("Unable to PrepDdevDirectory: %v", err) 233 } 234 235 app.SetApptypeSettingsPaths() 236 237 if app.DisableSettingsManagement && app.Type != nodeps.AppTypePHP { 238 util.Warning("Not creating CMS settings files because disable_settings_management=true") 239 return "", nil 240 } 241 242 // Drupal and WordPress love to change settings files to be unwriteable. 243 // Chmod them to something we can work with in the event that they already 244 // exist. 245 if app.SiteSettingsPath != "" { 246 chmodTargets := []string{filepath.Dir(app.SiteSettingsPath), app.SiteDdevSettingsFile} 247 for _, fp := range chmodTargets { 248 fileInfo, err := os.Stat(fp) 249 if err != nil { 250 // We're not doing anything about this error other than warning, 251 // and will have to deal with the same check in settingsCreator. 252 if !os.IsNotExist(err) { 253 util.Warning("Unable to ensure write permissions: %v", err) 254 } 255 256 continue 257 } 258 259 perms := 0644 260 if fileInfo.IsDir() { 261 perms = 0755 262 } 263 264 err = os.Chmod(fp, os.FileMode(perms)) 265 if err != nil { 266 return "", fmt.Errorf("could not change permissions on file %s to make it writeable: %v", fp, err) 267 } 268 } 269 } 270 271 // If we have a function to do the settings creation, do it, otherwise 272 // ignore it. 273 if appFuncs, ok := appTypeMatrix[app.GetType()]; ok && appFuncs.settingsCreator != nil { 274 settingsPath, err := appFuncs.settingsCreator(app) 275 if err != nil { 276 util.Warning("Unable to create settings file '%s': %v", app.SiteSettingsPath, err) 277 } 278 279 // Don't create gitignore if it would be in top-level directory, where 280 // there is almost certainly already a gitignore (like Backdrop) 281 if path.Dir(app.SiteSettingsPath) != app.AppRoot { 282 if err = CreateGitIgnore(filepath.Dir(app.SiteSettingsPath), filepath.Base(app.SiteDdevSettingsFile), "drushrc.php"); err != nil { 283 util.Warning("Failed to write .gitignore in %s: %v", filepath.Dir(app.SiteDdevSettingsFile), err) 284 } 285 } 286 return settingsPath, nil 287 } 288 289 // If the project is not running, it makes no sense to sync it 290 if s, _ := app.SiteStatus(); s == SiteRunning { 291 err = app.MutagenSyncFlush() 292 if err != nil { 293 return "", err 294 } 295 } 296 297 return "", nil 298 } 299 300 // GetHookDefaultComments gets the actual text of the config.yaml hook suggestions 301 // for a given apptype 302 func (app *DdevApp) GetHookDefaultComments() []byte { 303 if appFuncs, ok := appTypeMatrix[app.Type]; ok && appFuncs.hookDefaultComments != nil { 304 suggestions := appFuncs.hookDefaultComments() 305 return suggestions 306 } 307 return []byte("") 308 } 309 310 // GetComposerCreateAllowedPaths gets all paths relative to the app root that are allowed to be present 311 // for a given apptype when running ddev composer create 312 func (app *DdevApp) GetComposerCreateAllowedPaths() ([]string, error) { 313 var allowed []string 314 315 // doc root 316 allowed = append(allowed, nodeps.PathWithSlashesToArray(app.GetRelativeDirectory(app.GetDocroot()))...) 317 318 // composer root 319 allowed = append(allowed, nodeps.PathWithSlashesToArray(app.GetRelativeDirectory(app.GetComposerRoot(false, false)))...) 320 321 // allow upload dirs 322 // upload dirs are probably always relative and with slashes, but we run 323 // it through GetRelativeDirectory() just in case. 324 uploadDirs := app.getUploadDirsRelative() 325 for _, uploadDir := range uploadDirs { 326 allowed = append(allowed, nodeps.PathWithSlashesToArray(app.GetRelativeDirectory(uploadDir))...) 327 } 328 329 // Settings files 330 allowed = append(allowed, nodeps.PathWithSlashesToArray(app.GetRelativeDirectory(app.SiteSettingsPath))...) 331 allowed = append(allowed, nodeps.PathWithSlashesToArray(app.GetRelativeDirectory(app.SiteDdevSettingsFile))...) 332 333 // If we have a function to do the settings creation, allow .gitignore 334 // see CreateSettingsFile 335 if appFuncs, ok := appTypeMatrix[app.GetType()]; ok && appFuncs.settingsCreator != nil { 336 // We don't create gitignore if it would be in top-level directory, where 337 // there is almost certainly already a gitignore (like Backdrop) 338 if path.Dir(app.SiteSettingsPath) != app.AppRoot { 339 allowed = append(allowed, nodeps.PathWithSlashesToArray(app.GetRelativeDirectory(filepath.Join(filepath.Dir(app.SiteSettingsPath), ".gitignore")))...) 340 } 341 } 342 343 if appFuncs, ok := appTypeMatrix[app.Type]; ok && appFuncs.composerCreateAllowedPaths != nil { 344 paths, err := appFuncs.composerCreateAllowedPaths(app) 345 if err != nil { 346 return []string{""}, err 347 } 348 for _, path := range paths { 349 allowed = append(allowed, nodeps.PathWithSlashesToArray(app.GetRelativeDirectory(path))...) 350 } 351 } 352 allowed = util.SliceToUniqueSlice(&allowed) 353 sort.Strings(allowed) 354 return allowed, nil 355 } 356 357 // SetApptypeSettingsPaths chooses and sets the settings.php/settings.local.php 358 // and related paths for a given app. 359 func (app *DdevApp) SetApptypeSettingsPaths() { 360 if appFuncs, ok := appTypeMatrix[app.Type]; ok && appFuncs.appTypeSettingsPaths != nil { 361 appFuncs.appTypeSettingsPaths(app) 362 } 363 } 364 365 // DetectAppType calls each apptype's detector until it finds a match, 366 // or returns 'php' as a last resort. 367 func (app *DdevApp) DetectAppType() string { 368 for appTypeName, appFuncs := range appTypeMatrix { 369 if appFuncs.appTypeDetect != nil && appFuncs.appTypeDetect(app) { 370 return appTypeName 371 } 372 } 373 374 return nodeps.AppTypePHP 375 } 376 377 // PostImportDBAction calls each apptype's detector until it finds a match, 378 // or returns 'php' as a last resort. 379 func (app *DdevApp) PostImportDBAction() error { 380 381 if appFuncs, ok := appTypeMatrix[app.Type]; ok && appFuncs.postImportDBAction != nil { 382 return appFuncs.postImportDBAction(app) 383 } 384 385 return nil 386 } 387 388 // ConfigFileOverrideAction gives a chance for an apptype to override any element 389 // of config.yaml that it needs to 390 func (app *DdevApp) ConfigFileOverrideAction(overrideExistingConfig bool) error { 391 if appFuncs, ok := appTypeMatrix[app.Type]; ok && appFuncs.configOverrideAction != nil && (overrideExistingConfig || !app.ConfigExists()) { 392 origDB := app.Database 393 err := appFuncs.configOverrideAction(app) 394 if err != nil { 395 return err 396 } 397 // If the override function has changed the database type 398 // check to make sure that there's not one already existing 399 if origDB != app.Database { 400 // We can't upgrade database if it already exists 401 dbType, err := app.GetExistingDBType() 402 if err != nil { 403 return err 404 } 405 recommendedDBType := app.Database.Type + ":" + app.Database.Version 406 if dbType == "" { 407 // Assume that we don't have a database yet 408 util.Success("Configuring %s project with database type '%s'", app.Type, recommendedDBType) 409 } else if dbType != recommendedDBType { 410 util.Warning("%s project already has database type set to non-recommended: %s, not changing it to recommended %s", app.Type, dbType, recommendedDBType) 411 app.Database = origDB 412 } 413 } 414 } 415 416 return nil 417 } 418 419 // PostConfigAction gives a chance for an apptype to override do something at 420 // the end of ddev config. 421 func (app *DdevApp) PostConfigAction() error { 422 if appFuncs, ok := appTypeMatrix[app.Type]; ok && appFuncs.postConfigAction != nil { 423 return appFuncs.postConfigAction(app) 424 } 425 426 return nil 427 } 428 429 // PostStartAction gives a chance for an apptype to do something after the app 430 // has been started. 431 func (app *DdevApp) PostStartAction() error { 432 if appFuncs, ok := appTypeMatrix[app.Type]; ok && appFuncs.postStartAction != nil { 433 return appFuncs.postStartAction(app) 434 } 435 436 return nil 437 } 438 439 // dispatchImportFilesAction executes the relevant import files workflow for each app type. 440 func (app *DdevApp) dispatchImportFilesAction(uploadDir, importPath, extractPath string) error { 441 if strings.TrimSpace(uploadDir) == "" { 442 return errors.Errorf("upload_dirs is not set for this project (%s)", app.Type) 443 } 444 445 if appFuncs, ok := appTypeMatrix[app.Type]; ok { 446 // if a specific action is not defined, use a generic action 447 if appFuncs.importFilesAction == nil { 448 appFuncs.importFilesAction = genericImportFilesAction 449 } 450 return appFuncs.importFilesAction(app, uploadDir, importPath, extractPath) 451 } 452 453 return fmt.Errorf("this project type (%s) does not support import-files", app.Type) 454 } 455 456 // DefaultWorkingDirMap returns the app type's default working directory map. 457 func (app *DdevApp) DefaultWorkingDirMap() map[string]string { 458 _, _, username := util.GetContainerUIDGid() 459 // Default working directory values are defined here. 460 // Services working directories can be overridden by app types if needed. 461 defaults := map[string]string{ 462 "web": "/var/www/html/", 463 "db": "/home/" + username, 464 } 465 466 if appFuncs, ok := appTypeMatrix[app.Type]; ok && appFuncs.defaultWorkingDirMap != nil { 467 return appFuncs.defaultWorkingDirMap(app, defaults) 468 } 469 470 if app.Database.Type == nodeps.Postgres { 471 defaults["db"] = "/var/lib/postgresql" 472 } 473 return defaults 474 } 475 476 // docrootWorkingDir handles the shared case in which the web service working directory is the docroot. 477 func docrootWorkingDir(app *DdevApp, defaults map[string]string) map[string]string { 478 defaults["web"] = path.Join("/var/www/html", app.Docroot) 479 480 return defaults 481 } 482 483 // IsValidAppType is a helper function to determine if an app type is valid, returning 484 // true if the given app type is valid and configured and false otherwise. 485 func IsValidAppType(apptype string) bool { 486 if _, ok := appTypeMatrix[apptype]; !ok { 487 return false 488 } 489 490 return true 491 } 492 493 // GetValidAppTypes returns the valid apptype keys from the appTypeMatrix 494 func GetValidAppTypes() []string { 495 keys := make([]string, 0, len(appTypeMatrix)) 496 for k := range appTypeMatrix { 497 keys = append(keys, k) 498 sort.Sort(natural.StringSlice(keys)) 499 } 500 return keys 501 } 502 503 // GetValidAppTypesWithoutAliases returns the valid apptype keys from the appTypeMatrix without aliases like 504 // drupal8/9/10 505 func GetValidAppTypesWithoutAliases() []string { 506 keys := make([]string, 0, len(appTypeMatrix)) 507 for k := range appTypeMatrix { 508 if k == nodeps.AppTypeDrupal8 || k == nodeps.AppTypeDrupal9 || k == nodeps.AppTypeDrupal10 { 509 continue 510 } 511 keys = append(keys, k) 512 } 513 sort.Strings(keys) 514 return keys 515 }