diff --git a/cmd/mattermost/commands/config.go b/cmd/mattermost/commands/config.go index c04543fcda..be8fb56a0d 100644 --- a/cmd/mattermost/commands/config.go +++ b/cmd/mattermost/commands/config.go @@ -68,8 +68,19 @@ var ConfigSetCmd = &cobra.Command{ RunE: configSetCmdF, } +var MigrateConfigCmd = &cobra.Command{ + Use: "migrate", + Short: "Migrate existing config between backends", + Long: "Migrate a file-based configuration to (or from) a database-based configuration. Point the Mattermost server at the target configuration to start using it", + Example: `config migrate --from=path/to/config.json --to="postgres://mmuser:mostest@dockerhost:5432/mattermost_test?sslmode=disable&connect_timeout=10"`, + RunE: configMigrateCmdF, +} + func init() { ConfigSubpathCmd.Flags().String("path", "", "Optional subpath; defaults to value in SiteURL") + MigrateConfigCmd.Flags().String("from", "", "Config from which to migrate") + MigrateConfigCmd.Flags().String("to", "", "Config to which to migrate") + MigrateConfigCmd.MarkFlagRequired("to") ConfigCmd.AddCommand( ValidateConfigCmd, @@ -77,6 +88,7 @@ func init() { ConfigGetCmd, ConfigShowCmd, ConfigSetCmd, + MigrateConfigCmd, ) RootCmd.AddCommand(ConfigCmd) } @@ -217,6 +229,40 @@ func configSetCmdF(command *cobra.Command, args []string) error { return nil } +func configMigrateCmdF(command *cobra.Command, args []string) error { + // Parse source config; defaults to global --config unless overwritten by --from + from, err := command.Flags().GetString("from") + if err != nil { + return errors.Wrap(err, "failed reading source config parameter") + } + if from == "" { + from = viper.GetString("config") + } + + // Parse destination config store - MarkFlagRequired handles errors here + to, _ := command.Flags().GetString("to") + + // Get source config store - invalid config will throw error here + fromConfigStore, err := config.NewStore(from, false) + if err != nil { + return errors.Wrap(err, "failed to read --from config") + } + + // Get destination config store + toConfigStore, err := config.NewStore(to, false) + if err != nil { + return errors.Wrap(err, "failed to read --to config") + } + + // Copy config from source to destination + _, err = toConfigStore.Set(fromConfigStore.Get()) + if err != nil { + return errors.Wrap(err, "failed to migrate config") + } + + return nil +} + func updateConfigValue(configSetting string, newVal []string, oldConfig, newConfig *model.Config) func(*model.Config) { return func(update *model.Config) { diff --git a/cmd/mattermost/commands/config_test.go b/cmd/mattermost/commands/config_test.go index 4886095a21..ce04b85042 100644 --- a/cmd/mattermost/commands/config_test.go +++ b/cmd/mattermost/commands/config_test.go @@ -4,6 +4,8 @@ package commands import ( + "fmt" + "github.com/mattermost/mattermost-server/config" "io/ioutil" "os" "reflect" @@ -435,6 +437,43 @@ func TestUpdateMap(t *testing.T) { } +func TestConfigMigrate(t *testing.T) { + th := Setup() + defer th.TearDown() + + sqlSettings := mainHelper.GetSqlSettings() + sqlDSN := fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource) + fileDSN := "config.json" + + ds, err := config.NewStore(sqlDSN, false) + require.NoError(t, err) + fs, err := config.NewStore(fileDSN, false) + require.NoError(t, err) + + defer ds.Close() + defer fs.Close() + + t.Run("Should error without --to parameter", func(t *testing.T) { + assert.Error(t, th.RunCommand(t, "config", "migrate", "--from", fileDSN)) + }) + + t.Run("Should work passing the --to and the --from", func(t *testing.T) { + assert.NoError(t, th.RunCommand(t, "config", "migrate", "--from", fileDSN, "--to", sqlDSN)) + }) + + t.Run("Should work passing --to and no --from (taking the default config file)", func(t *testing.T) { + assert.NoError(t, th.RunCommand(t, "config", "migrate", "--to", sqlDSN)) + }) + + t.Run("Should fail passing an invalid --to", func(t *testing.T) { + assert.Error(t, th.RunCommand(t, "config", "migrate", "--from", fileDSN, "--to", "mysql://asd")) + }) + + t.Run("Should fail passing an invalid --from", func(t *testing.T) { + assert.Error(t, th.RunCommand(t, "config", "migrate", "--from", "invalid/path", "--to", sqlDSN)) + }) +} + func contains(configMap map[string]interface{}, v interface{}, configSettings []string) bool { res := configMap[configSettings[0]]