diff --git a/cmd/sync.go b/cmd/sync.go index 2e401db..2daa7c7 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -2,12 +2,12 @@ package cmd import ( "fmt" - "github.com/manifoldco/promptui" "github.com/mitchellh/mapstructure" "log" "os" "strings" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" "github.com/spf13/viper" synchers "github.com/uselagoon/lagoon-sync/synchers" @@ -29,120 +29,124 @@ var noCliInteraction bool var dryRun bool var verboseSSH bool var RsyncArguments string +var runSyncProcess synchers.RunSyncProcessFunctionType var syncCmd = &cobra.Command{ Use: "sync [mariadb|files|mongodb|postgres|etc.]", Short: "Sync a resource type", Long: `Use Lagoon-Sync to sync an external environments resources with the local environment`, Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { + Run: syncCommandRun, +} - SyncerType := args[0] - viper.Set("syncer-type", args[0]) +func syncCommandRun(cmd *cobra.Command, args []string) { - lagoonConfigBytestream, err := LoadLagoonConfig(cfgFile) - if err != nil { - utils.LogFatalError("Couldn't load lagoon config file - ", err.Error()) - } + SyncerType := args[0] + viper.Set("syncer-type", args[0]) - configRoot, err := synchers.UnmarshallLagoonYamlToLagoonSyncStructure(lagoonConfigBytestream) - if err != nil { - log.Fatalf("There was an issue unmarshalling the sync configuration from %v: %v", viper.ConfigFileUsed(), err) - } + lagoonConfigBytestream, err := LoadLagoonConfig(cfgFile) + if err != nil { + utils.LogFatalError("Couldn't load lagoon config file - ", err.Error()) + } - // If no project flag is given, find project from env var. - if ProjectName == "" { - project, exists := os.LookupEnv("LAGOON_PROJECT") - if exists { - ProjectName = strings.Replace(project, "_", "-", -1) - } - if configRoot.Project != "" { - ProjectName = configRoot.Project - } - } + configRoot, err := synchers.UnmarshallLagoonYamlToLagoonSyncStructure(lagoonConfigBytestream) + if err != nil { + log.Fatalf("There was an issue unmarshalling the sync configuration from %v: %v", viper.ConfigFileUsed(), err) + } - // Set service default to 'cli' - if ServiceName == "" { - ServiceName = getServiceName(SyncerType) + // If no project flag is given, find project from env var. + if ProjectName == "" { + project, exists := os.LookupEnv("LAGOON_PROJECT") + if exists { + ProjectName = strings.Replace(project, "_", "-", -1) } - - sourceEnvironment := synchers.Environment{ - ProjectName: ProjectName, - EnvironmentName: sourceEnvironmentName, - ServiceName: ServiceName, + if configRoot.Project != "" { + ProjectName = configRoot.Project } + } - // We assume that the target environment is local if it's not passed as an argument - if targetEnvironmentName == "" { - targetEnvironmentName = synchers.LOCAL_ENVIRONMENT_NAME - } - targetEnvironment := synchers.Environment{ - ProjectName: ProjectName, - EnvironmentName: targetEnvironmentName, - ServiceName: ServiceName, - } + // Set service default to 'cli' + if ServiceName == "" { + ServiceName = getServiceName(SyncerType) + } - var lagoonSyncer synchers.Syncer - lagoonSyncer, err = synchers.GetSyncerForTypeFromConfigRoot(SyncerType, configRoot) - if err != nil { - utils.LogFatalError(err.Error(), nil) - } + sourceEnvironment := synchers.Environment{ + ProjectName: ProjectName, + EnvironmentName: sourceEnvironmentName, + ServiceName: ServiceName, + } - if ProjectName == "" { - utils.LogFatalError("No Project name given", nil) - } + // We assume that the target environment is local if it's not passed as an argument + if targetEnvironmentName == "" { + targetEnvironmentName = synchers.LOCAL_ENVIRONMENT_NAME + } + targetEnvironment := synchers.Environment{ + ProjectName: ProjectName, + EnvironmentName: targetEnvironmentName, + ServiceName: ServiceName, + } - if !noCliInteraction { - confirmationResult, err := confirmPrompt(fmt.Sprintf("Project: %s - you are about to sync %s from %s to %s, is this correct", - ProjectName, - SyncerType, - sourceEnvironmentName, targetEnvironmentName)) - if err != nil || !confirmationResult { - utils.LogFatalError("User cancelled sync - exiting", nil) - } - } + var lagoonSyncer synchers.Syncer + lagoonSyncer, err = synchers.GetSyncerForTypeFromConfigRoot(SyncerType, configRoot) + if err != nil { + utils.LogFatalError(err.Error(), nil) + } - // SSH Config from file - sshConfig := synchers.SSHOptions{} - if configRoot.LagoonSync["ssh"] != nil { - mapstructure.Decode(configRoot.LagoonSync["ssh"], &sshConfig) - } - sshHost := SSHHost - if sshConfig.Host != "" && SSHHost == "ssh.lagoon.amazeeio.cloud" { - sshHost = sshConfig.Host - } - sshPort := SSHPort - if sshConfig.Port != "" && SSHPort == "32222" { - sshPort = sshConfig.Port - } + if ProjectName == "" { + utils.LogFatalError("No Project name given", nil) + } - sshKey := SSHKey - if sshConfig.PrivateKey != "" && SSHKey == "" { - sshKey = sshConfig.PrivateKey - } - sshVerbose := SSHVerbose - if sshConfig.Verbose && !sshVerbose { - sshVerbose = sshConfig.Verbose - } - sshOptions := synchers.SSHOptions{ - Host: sshHost, - PrivateKey: sshKey, - Port: sshPort, - Verbose: sshVerbose, - RsyncArgs: RsyncArguments, + if !noCliInteraction { + confirmationResult, err := confirmPrompt(fmt.Sprintf("Project: %s - you are about to sync %s from %s to %s, is this correct", + ProjectName, + SyncerType, + sourceEnvironmentName, targetEnvironmentName)) + if err != nil || !confirmationResult { + utils.LogFatalError("User cancelled sync - exiting", nil) } + } + + // SSH Config from file + sshConfig := synchers.SSHOptions{} + if configRoot.LagoonSync["ssh"] != nil { + mapstructure.Decode(configRoot.LagoonSync["ssh"], &sshConfig) + } + sshHost := SSHHost + if sshConfig.Host != "" && SSHHost == "ssh.lagoon.amazeeio.cloud" { + sshHost = sshConfig.Host + } + sshPort := SSHPort + if sshConfig.Port != "" && SSHPort == "32222" { + sshPort = sshConfig.Port + } - utils.LogDebugInfo("Config that is used for SSH", sshOptions) + sshKey := SSHKey + if sshConfig.PrivateKey != "" && SSHKey == "" { + sshKey = sshConfig.PrivateKey + } - err = synchers.RunSyncProcess(sourceEnvironment, targetEnvironment, lagoonSyncer, SyncerType, dryRun, sshOptions) - if err != nil { - utils.LogFatalError("There was an error running the sync process", err) - } + sshVerbose := SSHVerbose + if sshConfig.Verbose && !sshVerbose { + sshVerbose = sshConfig.Verbose + } + sshOptions := synchers.SSHOptions{ + Host: sshHost, + PrivateKey: sshKey, + Port: sshPort, + Verbose: sshVerbose, + RsyncArgs: RsyncArguments, + } - if !dryRun { - log.Printf("\n------\nSuccessful sync of %s from %s to %s\n------", SyncerType, sourceEnvironment.GetOpenshiftProjectName(), targetEnvironment.GetOpenshiftProjectName()) - } - }, + utils.LogDebugInfo("Config that is used for SSH", sshOptions) + + err = runSyncProcess(sourceEnvironment, targetEnvironment, lagoonSyncer, SyncerType, dryRun, sshOptions) + if err != nil { + utils.LogFatalError("There was an error running the sync process", err) + } + + if !dryRun { + log.Printf("\n------\nSuccessful sync of %s from %s to %s\n------", SyncerType, sourceEnvironment.GetOpenshiftProjectName(), targetEnvironment.GetOpenshiftProjectName()) + } } func getServiceName(SyncerType string) string { @@ -180,4 +184,8 @@ func init() { syncCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Don't run the commands, just preview what will be run") syncCmd.PersistentFlags().StringVarP(&RsyncArguments, "rsync-args", "r", "--omit-dir-times --no-perms --no-group --no-owner --chmod=ugo=rwX --recursive --compress", "Pass through arguments to change the behaviour of rsync") + // By default, we hook up the syncers.RunSyncProcess function to the runSyncProcess variable + // by doing this, it lets us easily override it for testing the command - but for most of the time + // this should be okay. + runSyncProcess = synchers.RunSyncProcess } diff --git a/cmd/sync_test.go b/cmd/sync_test.go new file mode 100644 index 0000000..9fea6a3 --- /dev/null +++ b/cmd/sync_test.go @@ -0,0 +1,76 @@ +package cmd + +import ( + "errors" + "fmt" + "github.com/spf13/cobra" + "github.com/uselagoon/lagoon-sync/synchers" + "testing" +) + +func Test_syncCommandRun(t *testing.T) { + type args struct { + cmd *cobra.Command + args []string + } + tests := []struct { + name string + lagoonYmlFile string + args args + runSyncProcess synchers.RunSyncProcessFunctionType //This will be the thing that drives the actual test + wantsError bool + }{ + { + name: "Tests defaults", + lagoonYmlFile: "../test-resources/sync-test/tests-defaults/.lagoon.yml", + args: args{ + cmd: nil, + args: []string{ + "mariadb", + }, + }, + runSyncProcess: func(sourceEnvironment synchers.Environment, targetEnvironment synchers.Environment, lagoonSyncer synchers.Syncer, syncerType string, dryRun bool, sshOptions synchers.SSHOptions) error { + if sshOptions.Port != "32222" { + return errors.New(fmt.Sprintf("Expecting ssh port 32222 - found: %v", sshOptions.Port)) + } + + if sshOptions.Host != "ssh.lagoon.amazeeio.cloud" { + return errors.New(fmt.Sprintf("Expecting ssh host ssh.lagoon.amazeeio.cloud - found: %v", sshOptions.Host)) + } + + return nil + }, + wantsError: false, + }, + { + name: "Tests Lagoon yaml", + lagoonYmlFile: "../test-resources/sync-test/tests-lagoon-yml/.lagoon.yml", + args: args{ + cmd: nil, + args: []string{ + "mariadb", + }, + }, + runSyncProcess: func(sourceEnvironment synchers.Environment, targetEnvironment synchers.Environment, lagoonSyncer synchers.Syncer, syncerType string, dryRun bool, sshOptions synchers.SSHOptions) error { + if sshOptions.Port != "777" { + return errors.New(fmt.Sprintf("Expecting ssh port 777 - found: %v", sshOptions.Port)) + } + + if sshOptions.Host != "example.ssh.lagoon.amazeeio.cloud" { + return errors.New(fmt.Sprintf("Expecting ssh host ssh.lagoon.amazeeio.cloud - found: %v", sshOptions.Host)) + } + + return nil + }, + wantsError: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runSyncProcess = tt.runSyncProcess + cfgFile = tt.lagoonYmlFile + noCliInteraction = true + syncCommandRun(tt.args.cmd, tt.args.args) + }) + } +} diff --git a/synchers/syncutils.go b/synchers/syncutils.go index 95886b6..757dc0c 100644 --- a/synchers/syncutils.go +++ b/synchers/syncutils.go @@ -27,6 +27,8 @@ func UnmarshallLagoonYamlToLagoonSyncStructure(data []byte) (SyncherConfigRoot, return lagoonConfig, nil } +type RunSyncProcessFunctionType = func(sourceEnvironment Environment, targetEnvironment Environment, lagoonSyncer Syncer, syncerType string, dryRun bool, sshOptions SSHOptions) error + func RunSyncProcess(sourceEnvironment Environment, targetEnvironment Environment, lagoonSyncer Syncer, syncerType string, dryRun bool, sshOptions SSHOptions) error { var err error diff --git a/test-resources/sync-test/tests-defaults/.lagoon.yml b/test-resources/sync-test/tests-defaults/.lagoon.yml new file mode 100644 index 0000000..6735aa3 --- /dev/null +++ b/test-resources/sync-test/tests-defaults/.lagoon.yml @@ -0,0 +1,67 @@ +# Example .lagoon.yml file with lagoon-sync config added which is used by the sync tool. +docker-compose-yaml: docker-compose.yml + +project: "lagoon-sync" + +lagoon-sync: + mariadb: + config: + hostname: "$MARIADB_HOST" + username: "$MARIADB_USERNAME" + password: "$MARIADB_PASSWORD" + port: "$MARIADB_PORT" + database: "$MARIADB_DATABASE" + ignore-table: + - "table_to_ignore" + ignore-table-data: + - "cache_data" + - "cache_menu" + local: + config: + hostname: "mariadb" + username: "drupal" + password: "drupal" + port: "3306" + database: "drupal" + postgres: + config: + hostname: "$POSTGRES_HOST" + username: "$POSTGRES_USERNAME" + password: "$POSTGRES_PASSWORD" + port: "5432" + database: "$POSTGRES_DATABASE" + exclude-table: + - "table_to_ignore" + exclude-table-data: + - "cache_data" + - "cache_menu" + local: + config: + hostname: "postgres" + username: "drupal" + password: "drupal" + port: "3306" + database: "drupal" + mongodb: + config: + hostname: "$MONGODB_HOST" + port: "$MONGODB_SERVICE_PORT" + database: "MONGODB_DATABASE" + local: + config: + hostname: "$MONGODB_HOST" + port: "27017" + database: "local" + files: + config: + sync-directory: "/app/web/sites/default/files" + local: + config: + sync-directory: "/app/web/sites/default/files" + drupalconfig: + config: + syncpath: "./config/sync" + local: + overrides: + config: + syncpath: "./config/sync" diff --git a/test-resources/sync-test/tests-lagoon-yml/.lagoon.yml b/test-resources/sync-test/tests-lagoon-yml/.lagoon.yml new file mode 100644 index 0000000..6b68b96 --- /dev/null +++ b/test-resources/sync-test/tests-lagoon-yml/.lagoon.yml @@ -0,0 +1,72 @@ +# Example .lagoon.yml file with lagoon-sync config added which is used by the sync tool. +docker-compose-yaml: docker-compose.yml + +project: "lagoon-sync" + +lagoon-sync: + ssh: + host: "example.ssh.lagoon.amazeeio.cloud" + port: "777" + privateKey: "~/.ssh/example_id_rsa" + verbose: true + mariadb: + config: + hostname: "$MARIADB_HOST" + username: "$MARIADB_USERNAME" + password: "$MARIADB_PASSWORD" + port: "$MARIADB_PORT" + database: "$MARIADB_DATABASE" + ignore-table: + - "table_to_ignore" + ignore-table-data: + - "cache_data" + - "cache_menu" + local: + config: + hostname: "mariadb" + username: "drupal" + password: "drupal" + port: "3306" + database: "drupal" + postgres: + config: + hostname: "$POSTGRES_HOST" + username: "$POSTGRES_USERNAME" + password: "$POSTGRES_PASSWORD" + port: "5432" + database: "$POSTGRES_DATABASE" + exclude-table: + - "table_to_ignore" + exclude-table-data: + - "cache_data" + - "cache_menu" + local: + config: + hostname: "postgres" + username: "drupal" + password: "drupal" + port: "3306" + database: "drupal" + mongodb: + config: + hostname: "$MONGODB_HOST" + port: "$MONGODB_SERVICE_PORT" + database: "MONGODB_DATABASE" + local: + config: + hostname: "$MONGODB_HOST" + port: "27017" + database: "local" + files: + config: + sync-directory: "/app/web/sites/default/files" + local: + config: + sync-directory: "/app/web/sites/default/files" + drupalconfig: + config: + syncpath: "./config/sync" + local: + overrides: + config: + syncpath: "./config/sync"