github.com/ddev/ddev@v1.23.2-0.20240519125000-d824ffe36ff3/pkg/ddevapp/provider.go (about) 1 package ddevapp 2 3 import ( 4 "fmt" 5 "os" 6 "path" 7 "path/filepath" 8 "strings" 9 10 "github.com/ddev/ddev/pkg/fileutil" 11 "github.com/ddev/ddev/pkg/output" 12 "github.com/ddev/ddev/pkg/util" 13 "gopkg.in/yaml.v3" 14 ) 15 16 // ProviderCommand defines the shell command to be run for one of the commands (db pull, etc.) 17 type ProviderCommand struct { 18 Command string `yaml:"command"` 19 Service string `yaml:"service,omitempty"` 20 } 21 22 // ProviderInfo defines the provider 23 type ProviderInfo struct { 24 EnvironmentVariables map[string]string `yaml:"environment_variables"` 25 AuthCommand ProviderCommand `yaml:"auth_command"` 26 DBPullCommand ProviderCommand `yaml:"db_pull_command"` 27 DBImportCommand ProviderCommand `yaml:"db_import_command"` 28 FilesPullCommand ProviderCommand `yaml:"files_pull_command"` 29 FilesImportCommand ProviderCommand `yaml:"files_import_command"` 30 CodePullCommand ProviderCommand `yaml:"code_pull_command,omitempty"` 31 DBPushCommand ProviderCommand `yaml:"db_push_command"` 32 FilesPushCommand ProviderCommand `yaml:"files_push_command"` 33 } 34 35 // Provider provides generic-specific import functionality. 36 type Provider struct { 37 ProviderType string `yaml:"provider"` 38 app *DdevApp `yaml:"-"` 39 ProviderInfo `yaml:"providers"` 40 } 41 42 // Init handles loading data from saved config. 43 func (p *Provider) Init(pType string, app *DdevApp) error { 44 p.app = app 45 configPath := app.GetConfigPath(filepath.Join("providers", pType+".yaml")) 46 if !fileutil.FileExists(configPath) { 47 return fmt.Errorf("no configuration exists for %s provider - it should be at %s", pType, configPath) 48 } 49 err := p.Read(configPath) 50 if err != nil { 51 return err 52 } 53 54 if p.EnvironmentVariables == nil { 55 p.EnvironmentVariables = map[string]string{} 56 } 57 58 p.ProviderType = pType 59 app.ProviderInstance = p 60 return nil 61 } 62 63 // Pull performs an import of db and files 64 func (app *DdevApp) Pull(provider *Provider, skipDbArg bool, skipFilesArg bool, skipImportArg bool) error { 65 var err error 66 err = app.ProcessHooks("pre-pull") 67 if err != nil { 68 return fmt.Errorf("failed to process pre-pull hooks: %v", err) 69 } 70 71 status, _ := app.SiteStatus() 72 if status != SiteRunning { 73 util.Warning("Project is not currently running. Starting project before performing pull.") 74 err = app.Start() 75 if err != nil { 76 return err 77 } 78 } 79 80 if provider.AuthCommand.Command != "" { 81 output.UserOut.Print("Authenticating...") 82 err = provider.app.ExecOnHostOrService(provider.AuthCommand.Service, provider.injectedEnvironment()+"; "+provider.AuthCommand.Command) 83 if err != nil { 84 return err 85 } 86 } 87 88 if skipDbArg { 89 output.UserOut.Println("Skipping database pull.") 90 } else { 91 output.UserOut.Println("Obtaining databases...") 92 fileLocation, importPath, err := provider.GetBackup("database") 93 if err != nil { 94 return err 95 } 96 err = app.MutagenSyncFlush() 97 if err != nil { 98 return err 99 } 100 101 if skipImportArg { 102 output.UserOut.Println("Skipping database import.") 103 } else { 104 err = app.MutagenSyncFlush() 105 if err != nil { 106 return err 107 } 108 output.UserOut.Printf("Importing databases %v\n", fileLocation) 109 err = provider.importDatabaseBackup(fileLocation, importPath) 110 if err != nil { 111 return err 112 } 113 } 114 } 115 116 if skipFilesArg { 117 output.UserOut.Println("Skipping files pull.") 118 } else { 119 output.UserOut.Println("Obtaining files...") 120 files, _, err := provider.GetBackup("files") 121 if err != nil { 122 return err 123 } 124 125 err = app.MutagenSyncFlush() 126 if err != nil { 127 return err 128 } 129 130 if skipImportArg { 131 output.UserOut.Println("Skipping files import.") 132 } else { 133 output.UserOut.Println("Importing files...") 134 f := "" 135 if files != nil && len(files) > 0 { 136 f = files[0] 137 } 138 err = provider.doFilesImport(f, "") 139 if err != nil { 140 return err 141 } 142 } 143 } 144 err = app.ProcessHooks("post-pull") 145 if err != nil { 146 return fmt.Errorf("failed to process post-pull hooks: %v", err) 147 } 148 149 return nil 150 } 151 152 // Push pushes db and files up to upstream hosting provider 153 func (app *DdevApp) Push(provider *Provider, skipDbArg bool, skipFilesArg bool) error { 154 var err error 155 err = app.ProcessHooks("pre-push") 156 if err != nil { 157 return fmt.Errorf("failed to process pre-push hooks: %v", err) 158 } 159 160 status, _ := app.SiteStatus() 161 if status != SiteRunning { 162 util.Warning("Project is not currently running. Starting project before performing push.") 163 err = app.Start() 164 if err != nil { 165 return err 166 } 167 } 168 169 if provider.AuthCommand.Command != "" { 170 output.UserOut.Print("Authenticating...") 171 err := provider.app.ExecOnHostOrService(provider.AuthCommand.Service, provider.injectedEnvironment()+"; "+provider.AuthCommand.Command) 172 if err != nil { 173 return err 174 } 175 } 176 177 if skipDbArg { 178 output.UserOut.Println("Skipping database push.") 179 } else { 180 output.UserOut.Println("Uploading database...") 181 err = provider.UploadDB() 182 if err != nil { 183 return err 184 } 185 186 output.UserOut.Printf("Database uploaded") 187 } 188 189 if skipFilesArg { 190 output.UserOut.Println("Skipping files push.") 191 } else { 192 output.UserOut.Println("Uploading files...") 193 err = provider.UploadFiles() 194 if err != nil { 195 return err 196 } 197 198 output.UserOut.Printf("Files uploaded") 199 } 200 err = app.ProcessHooks("post-push") 201 if err != nil { 202 return fmt.Errorf("failed to process post-push hooks: %v", err) 203 } 204 205 return nil 206 } 207 208 // GetBackup will create and download a set of backups 209 // Valid values for backupType are "database" or "files". 210 // returns []fileURL, []importPath, error 211 func (p *Provider) GetBackup(backupType string) ([]string, []string, error) { 212 var err error 213 var fileNames []string 214 if backupType != "database" && backupType != "files" { 215 return nil, nil, fmt.Errorf("could not get backup: %s is not a valid backup type", backupType) 216 } 217 218 p.prepDownloadDir() 219 220 switch backupType { 221 case "database": 222 fileNames, err = p.getDatabaseBackups() 223 case "files": 224 fileNames, err = p.doFilesPullCommand() 225 default: 226 return nil, nil, fmt.Errorf("could not get backup: %s is not a valid backup type", backupType) 227 } 228 if err != nil { 229 return nil, nil, err 230 } 231 232 importPaths := make([]string, len(fileNames)) 233 // We don't use importPaths for the providers 234 for i := range fileNames { 235 importPaths[i] = "" 236 } 237 238 return fileNames, importPaths, nil 239 } 240 241 // UploadDB is used by Push to push the database to hosting provider 242 func (p *Provider) UploadDB() error { 243 _ = os.RemoveAll(p.getDownloadDir()) 244 _ = os.Mkdir(p.getDownloadDir(), 0755) 245 246 if p.DBPushCommand.Command == "" { 247 return nil 248 } 249 250 err := p.app.ExportDB(p.app.GetConfigPath(".downloads/db.sql.gz"), "gzip", "") 251 if err != nil { 252 return err 253 } 254 err = p.app.MutagenSyncFlush() 255 if err != nil { 256 return err 257 } 258 259 s := p.DBPushCommand.Service 260 if s == "" { 261 s = "web" 262 } 263 err = p.app.ExecOnHostOrService(s, p.injectedEnvironment()+"; "+p.DBPushCommand.Command) 264 if err != nil { 265 return fmt.Errorf("failed to exec %s on %s: %v", p.DBPushCommand.Command, s, err) 266 } 267 return nil 268 } 269 270 // UploadFiles is used by Push to push the user-generated files to the hosting provider 271 func (p *Provider) UploadFiles() error { 272 _ = os.RemoveAll(p.getDownloadDir()) 273 _ = os.Mkdir(p.getDownloadDir(), 0755) 274 275 if p.FilesPushCommand.Command == "" { 276 return nil 277 } 278 279 s := p.FilesPushCommand.Service 280 if s == "" { 281 s = "web" 282 } 283 err := p.app.ExecOnHostOrService(s, p.injectedEnvironment()+"; "+p.FilesPushCommand.Command) 284 if err != nil { 285 return fmt.Errorf("failed to exec %s on %s: %v", p.FilesPushCommand.Command, s, err) 286 } 287 return nil 288 } 289 290 // prepDownloadDir ensures the download cache directories are created and writeable. 291 func (p *Provider) prepDownloadDir() { 292 destDir := p.getDownloadDir() 293 filesDir := filepath.Join(destDir, "files") 294 _ = os.RemoveAll(destDir) 295 err := os.MkdirAll(filesDir, 0755) 296 util.CheckErr(err) 297 } 298 299 func (p *Provider) getDownloadDir() string { 300 destDir := p.app.GetConfigPath(".downloads") 301 return destDir 302 } 303 304 func (p *Provider) doFilesPullCommand() (filename []string, error error) { 305 destDir := filepath.Join(p.getDownloadDir(), "files") 306 _ = os.RemoveAll(destDir) 307 _ = os.MkdirAll(destDir, 0755) 308 309 if p.FilesPullCommand.Command == "" { 310 return nil, nil 311 } 312 s := p.FilesPullCommand.Service 313 if s == "" { 314 s = "web" 315 } 316 317 err := p.app.ExecOnHostOrService(s, p.injectedEnvironment()+"; "+p.FilesPullCommand.Command) 318 if err != nil { 319 return nil, fmt.Errorf("failed to exec %s on %s: %v", p.FilesPullCommand.Command, s, err) 320 } 321 322 return []string{filepath.Join(p.getDownloadDir(), "files")}, nil 323 } 324 325 // getDatabaseBackups retrieves database using `generic backup database`, then 326 // describe until it appears, then download it. 327 func (p *Provider) getDatabaseBackups() (filename []string, error error) { 328 err := os.RemoveAll(p.getDownloadDir()) 329 if err != nil { 330 return nil, err 331 } 332 err = os.Mkdir(p.getDownloadDir(), 0755) 333 if err != nil { 334 return nil, err 335 } 336 337 if p.DBPullCommand.Command == "" { 338 return nil, nil 339 } 340 341 s := p.DBPullCommand.Service 342 if s == "" { 343 s = "web" 344 } 345 err = p.app.MutagenSyncFlush() 346 if err != nil { 347 return nil, err 348 } 349 err = p.app.ExecOnHostOrService(s, p.injectedEnvironment()+"; "+p.DBPullCommand.Command) 350 if err != nil { 351 return nil, fmt.Errorf("failed to exec %s on %s: %v", p.DBPullCommand.Command, s, err) 352 } 353 err = p.app.MutagenSyncFlush() 354 if err != nil { 355 return nil, err 356 } 357 358 sqlTarballs, err := fileutil.ListFilesInDirFullPath(p.getDownloadDir()) 359 if err != nil || sqlTarballs == nil { 360 return nil, fmt.Errorf("failed to find downloaded files in %s: %v", p.getDownloadDir(), err) 361 } 362 return sqlTarballs, nil 363 } 364 365 // importDatabaseBackup will import a slice of downloaded databases 366 // If a custom importer is provided, that will be used, otherwise 367 // the default is app.ImportDB() 368 func (p *Provider) importDatabaseBackup(fileLocation []string, importPath []string) error { 369 var err error 370 if p.DBImportCommand.Command == "" { 371 for i, loc := range fileLocation { 372 // The database name used will be basename of the file. 373 // For example. `db.sql.gz` will go into the database named 'db' 374 // xxx.sql will go into database named 'xxx'; 375 b := path.Base(loc) 376 n := strings.Split(b, ".") 377 dbName := n[0] 378 err = p.app.ImportDB(loc, importPath[i], true, false, dbName) 379 } 380 } else { 381 s := p.DBImportCommand.Service 382 if s == "" { 383 s = "web" 384 } 385 output.UserOut.Printf("Importing database via custom db_import_command") 386 err = p.app.ExecOnHostOrService(s, p.injectedEnvironment()+"; "+p.DBImportCommand.Command) 387 } 388 return err 389 } 390 391 // doFilesImport will import previously downloaded files tarball or directory 392 // If a custom importer (FileImportCommand) is provided, that will be used, otherwise 393 // the default is app.ImportFiles() 394 // FilesImportCommand may also optionally take on the job of downloading the files. 395 func (p *Provider) doFilesImport(fileLocation string, importPath string) error { 396 var err error 397 if p.FilesImportCommand.Command == "" { 398 err = p.app.ImportFiles("", fileLocation, importPath) 399 } else { 400 s := p.FilesImportCommand.Service 401 if s == "" { 402 s = "web" 403 } 404 output.UserOut.Printf("Importing files via custom files_import_command...") 405 err = p.app.ExecOnHostOrService(s, p.injectedEnvironment()+"; "+p.FilesImportCommand.Command) 406 } 407 return err 408 } 409 410 // Read generic provider configuration from a specified location on disk. 411 func (p *Provider) Read(configPath string) error { 412 source, err := os.ReadFile(configPath) 413 if err != nil { 414 return err 415 } 416 417 // Read config values from file. 418 err = yaml.Unmarshal(source, &p.ProviderInfo) 419 if err != nil { 420 return err 421 } 422 423 return nil 424 } 425 426 // Validate ensures that the current configuration is valid (i.e. the configured pantheon site/environment exists) 427 func (p *Provider) Validate() error { 428 return nil 429 } 430 431 // injectedEnvironment() returns a string with environment variables that should be injected 432 // before a command. 433 func (p *Provider) injectedEnvironment() string { 434 s := "true" 435 if len(p.EnvironmentVariables) > 0 { 436 s = "export" 437 for k, v := range p.EnvironmentVariables { 438 v = strings.Replace(v, " ", `\ `, -1) 439 s = s + fmt.Sprintf(" %s=%s ", k, v) 440 } 441 } 442 return s 443 }