* 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
Этот коммит содержится в:
коммит произвёл
George Goldberg
родитель
937b6480d5
Коммит
2b6d6acb78
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -58,6 +59,15 @@ var ConfigShowCmd = &cobra.Command{
|
|||||||
RunE: configShowCmdF,
|
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() {
|
func init() {
|
||||||
ConfigSubpathCmd.Flags().String("path", "", "Optional subpath; defaults to value in SiteURL")
|
ConfigSubpathCmd.Flags().String("path", "", "Optional subpath; defaults to value in SiteURL")
|
||||||
|
|
||||||
@@ -66,6 +76,7 @@ func init() {
|
|||||||
ConfigSubpathCmd,
|
ConfigSubpathCmd,
|
||||||
ConfigGetCmd,
|
ConfigGetCmd,
|
||||||
ConfigShowCmd,
|
ConfigShowCmd,
|
||||||
|
ConfigSetCmd,
|
||||||
)
|
)
|
||||||
RootCmd.AddCommand(ConfigCmd)
|
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
|
// configToMap converts our config into a map
|
||||||
func configToMap(s interface{}) map[string]interface{} {
|
func configToMap(s interface{}) map[string]interface{} {
|
||||||
return structToMap(s)
|
return structToMap(s)
|
||||||
|
|||||||
@@ -54,6 +54,24 @@ type TestClientRequirements struct {
|
|||||||
Desktoplatestversion string
|
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) {
|
func TestConfigValidate(t *testing.T) {
|
||||||
dir, err := ioutil.TempDir("", "")
|
dir, err := ioutil.TempDir("", "")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -416,5 +434,122 @@ func TestConfigShow(t *testing.T) {
|
|||||||
assert.Contains(t, string(output), "SqlSettings")
|
assert.Contains(t, string(output), "SqlSettings")
|
||||||
assert.Contains(t, string(output), "MessageExportSettings")
|
assert.Contains(t, string(output), "MessageExportSettings")
|
||||||
assert.Contains(t, string(output), "AnnouncementSettings")
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user