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  }