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  }