[MM-12361] Create CLI-command "config set" (#9535) (#9582)

* Create the config set command in the variable

* Register the command and declare the command in the run function

* Finish implementation of 'config set' cli command

* Write tests for config set command

* Change minimum number of arguments to 2

* Correct changes

* Correct error problem

* Update the command description and errors

* Refactor function name and improve error messages

* Write test for UpdateMap function
Этот коммит содержится в:
Mukul Rawat
2018-10-29 14:59:15 +05:30
коммит произвёл George Goldberg
родитель 937b6480d5
Коммит 2b6d6acb78
2 изменённых файлов: 285 добавлений и 0 удалений

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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
}
}