diff --git a/cmd/mattermost/commands/config.go b/cmd/mattermost/commands/config.go index c40debfd32..39c12fc312 100644 --- a/cmd/mattermost/commands/config.go +++ b/cmd/mattermost/commands/config.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "reflect" + "strconv" "strings" "github.com/pkg/errors" @@ -58,6 +59,15 @@ var ConfigShowCmd = &cobra.Command{ RunE: configShowCmdF, } +var ConfigSetCmd = &cobra.Command{ + Use: "set", + Short: "Set config setting", + Long: "Sets the value of a config setting by its name in dot notation. Accepts multiple values for array settings", + Example: "config set SqlSettings.DriverName mysql", + Args: cobra.MinimumNArgs(2), + RunE: configSetCmdF, +} + func init() { ConfigSubpathCmd.Flags().String("path", "", "Optional subpath; defaults to value in SiteURL") @@ -66,6 +76,7 @@ func init() { ConfigSubpathCmd, ConfigGetCmd, ConfigShowCmd, + ConfigSetCmd, ) RootCmd.AddCommand(ConfigCmd) } @@ -224,6 +235,145 @@ func printMap(value reflect.Value, tabVal int) string { } +func configSetCmdF(command *cobra.Command, args []string) error { + app, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + + defer app.Shutdown() + + // args[0] -> holds the config setting that we want to change + // args[1:] -> the new value of the config setting + configSetting := args[0] + newVal := args[1:] + + // Update the config + + // first disable the watchers + app.DisableConfigWatch() + + // create the function to update config + oldConfig := app.Config() + newConfig := app.Config() + f := updateConfigValue(configSetting, newVal, oldConfig, newConfig) + + // update the config + app.UpdateConfig(f) + + // make the changes persist + app.PersistConfig() + + // reload config + app.ReloadConfig() + + // Enable config watchers + app.EnableConfigWatch() + + return nil +} + +func updateConfigValue(configSetting string, newVal []string, oldConfig, newConfig *model.Config) func(*model.Config) { + return func(update *model.Config) { + + // convert config to map[string]interface + configMap := configToMap(*oldConfig) + + // iterate through the map and update the value or print an error and exit + err := UpdateMap(configMap, strings.Split(configSetting, "."), newVal) + if err != nil { + fmt.Printf("%s\n", err) + os.Exit(1) + } + + // convert map to json + bs, err := json.Marshal(configMap) + if err != nil { + fmt.Printf("Error while marshalling map to json %s\n", err) + os.Exit(1) + } + + // convert json to struct + err = json.Unmarshal(bs, newConfig) + if err != nil { + fmt.Printf("Error while unmarshalling json to struct %s\n", err) + os.Exit(1) + } + + *update = *newConfig + + } +} + +func UpdateMap(configMap map[string]interface{}, configSettings []string, newVal []string) error { + res, ok := configMap[configSettings[0]] + if !ok { + return fmt.Errorf("unable to find a setting with that name %s", configSettings[0]) + } + + value := reflect.ValueOf(res) + + switch value.Kind() { + + case reflect.Map: + // we can only change the value of a particular setting, not the whole map, return error + if len(configSettings) == 1 { + return errors.New("unable to set multiple settings at once") + } + return UpdateMap(res.(map[string]interface{}), configSettings[1:], newVal) + + case reflect.Int: + if len(configSettings) == 1 { + val, err := strconv.Atoi(newVal[0]) + if err != nil { + return err + } + configMap[configSettings[0]] = val + return nil + } + return fmt.Errorf("unable to find a setting with that name %s", configSettings[0]) + + case reflect.Int64: + if len(configSettings) == 1 { + val, err := strconv.Atoi(newVal[0]) + if err != nil { + return err + } + configMap[configSettings[0]] = int64(val) + return nil + } + return fmt.Errorf("unable to find a setting with that name %s", configSettings[0]) + + case reflect.Bool: + if len(configSettings) == 1 { + val, err := strconv.ParseBool(newVal[0]) + if err != nil { + return err + } + configMap[configSettings[0]] = val + return nil + } + return fmt.Errorf("unable to find a setting with that name %s", configSettings[0]) + + case reflect.String: + if len(configSettings) == 1 { + configMap[configSettings[0]] = newVal[0] + return nil + } + return fmt.Errorf("unable to find a setting with that name %s", configSettings[0]) + + case reflect.Slice: + if len(configSettings) == 1 { + configMap[configSettings[0]] = newVal + return nil + } + return fmt.Errorf("unable to find a setting with that name %s", configSettings[0]) + + default: + return errors.New("type not supported yet") + } +} + // configToMap converts our config into a map func configToMap(s interface{}) map[string]interface{} { return structToMap(s) diff --git a/cmd/mattermost/commands/config_test.go b/cmd/mattermost/commands/config_test.go index 02178c7700..8982d1ce77 100644 --- a/cmd/mattermost/commands/config_test.go +++ b/cmd/mattermost/commands/config_test.go @@ -54,6 +54,24 @@ type TestClientRequirements struct { Desktoplatestversion string } +type TestNewConfig struct { + TestNewServiceSettings TestNewServiceSettings + TestNewTeamSettings TestNewTeamSettings +} + +type TestNewServiceSettings struct{ + SiteUrl *string + UseLetsEncrypt *bool + TLSStrictTransportMaxAge *int64 + AllowedThemes []string +} + + +type TestNewTeamSettings struct { + SiteName *string + MaxUserPerTeam *int +} + func TestConfigValidate(t *testing.T) { dir, err := ioutil.TempDir("", "") require.NoError(t, err) @@ -416,5 +434,122 @@ func TestConfigShow(t *testing.T) { assert.Contains(t, string(output), "SqlSettings") assert.Contains(t, string(output), "MessageExportSettings") assert.Contains(t, string(output), "AnnouncementSettings") +} + +func TestSetConfig(t *testing.T) { + // Error when no argument is given + assert.Error(t, RunCommand(t, "config", "set")) + + // No Error when more than one argument is given + assert.NoError(t, RunCommand(t, "config", "set", "ThemeSettings.AllowedThemes", "hello", "World")) + + // No Error when two arguments are given + assert.NoError(t, RunCommand(t, "config", "set", "ThemeSettings.AllowedThemes", "hello")) + + // Error when only one argument is given + assert.Error(t, RunCommand(t, "config", "set", "ThemeSettings.AllowedThemes")) + + // Error when config settings not in the config file are given + assert.Error(t, RunCommand(t, "config", "set", "Abc")) +} + +func TestUpdateMap(t *testing.T) { + + // create a config to make changes + config := TestNewConfig{ + TestNewServiceSettings{ + SiteUrl: model.NewString("abc.def"), + UseLetsEncrypt: model.NewBool(false), + TLSStrictTransportMaxAge: model.NewInt64(36), + AllowedThemes: []string{"Hello", "World"}, + }, + TestNewTeamSettings{ + SiteName: model.NewString("def.ghi"), + MaxUserPerTeam: model.NewInt(12), + }, + } + + + // create a map of type map[string]interface + configMap := configToMap(config) + + cases := []struct{ + Name string + configSettings []string + newVal []string + expected interface{} + } { + { + Name: "check for Map and string", + configSettings: []string{"TestNewServiceSettings", "SiteUrl"}, + newVal: []string{"siteurl"}, + expected: "siteurl", + }, + { + Name: "check for Map and bool", + configSettings: []string{"TestNewServiceSettings", "UseLetsEncrypt"}, + newVal: []string{"true"}, + expected: true, + }, + { + Name: "check for Map and int64", + configSettings: []string{"TestNewServiceSettings", "TLSStrictTransportMaxAge"}, + newVal: []string{"56"}, + expected: int64(56), + }, + { + Name: "check for Map and string Slice", + configSettings: []string{"TestNewServiceSettings", "AllowedThemes"}, + newVal: []string{"hello1", "world1"}, + expected: []string{"hello1", "world1"}, + }, + { + Name: "Map and string", + configSettings: []string{"TestNewTeamSettings", "SiteName"}, + newVal: []string{"jkl.mno"}, + expected: "jkl.mno", + }, + { + Name: "Map and int", + configSettings: []string{"TestNewTeamSettings", "MaxUserPerTeam"}, + newVal: []string{"18"}, + expected: 18, + }, + } + + + for _, test := range cases { + + t.Run(test.Name, func(t *testing.T){ + err := UpdateMap(configMap, test.configSettings, test.newVal) + + if err != nil { + t.Fatal("Wasn't expecting an error: ", err) + } + + if !contains(configMap, test.expected, test.configSettings) { + t.Error("update didn't happen") + } + + }) + } } + +func contains(configMap map[string]interface{}, v interface{}, configSettings []string) bool { + + res := configMap[configSettings[0]] + + value := reflect.ValueOf(res) + + switch value.Kind() { + case reflect.Map: + return contains(res.(map[string]interface{}), v, configSettings[1:]) + case reflect.Slice: + return reflect.DeepEqual(value.Interface(), v) + case reflect.Int64: + return value.Interface() == v.(int64) + default: + return value.Interface() == v + } +}