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