From 9385dc750d59a4dcac168cbdba926892a467ad1d Mon Sep 17 00:00:00 2001 From: Mukul Rawat Date: Mon, 15 Oct 2018 20:19:25 +0530 Subject: [MM-12360] Created CLI command "config get" (#9534) (#9558) * Added the get command to get the value of a config setting. * Depending on the config setting it can work on any depth of the setting. * Added test for the get command. * Add print tabs * Remove excess else statements * Return with the value and remove named return variable * Refactor the printMap function and return a string, remove side effects * Improve the error message, use the name argument * Use app.Config() to create our config object * Remove reading the file, make helper functions return string and perform printing inside the command * Remove the tab printing * Add extra quotes on the output * Remove extra code for checking arguments and replaced it with cobra.ExactArgs(1) * Remove buffer from printConfigValues * Add some tests to check the output of the command * Write test for the function 'structToMap' and test for complext nested structs * Write test for the function 'configToMap' and test for complext nested structs * Write test for the function 'printMap' and test for complext maps as input * Write test for the function 'printConfigValues' and test for complext maps as input * Remove commented code * Update the description of the command --- cmd/mattermost/commands/config.go | 132 ++++++++++++ cmd/mattermost/commands/config_test.go | 375 +++++++++++++++++++++++++++++++++ 2 files changed, 507 insertions(+) diff --git a/cmd/mattermost/commands/config.go b/cmd/mattermost/commands/config.go index b8630b82f..47dd61458 100644 --- a/cmd/mattermost/commands/config.go +++ b/cmd/mattermost/commands/config.go @@ -4,12 +4,17 @@ package commands import ( + "bytes" "encoding/json" + "fmt" "os" + "reflect" + "strings" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" ) @@ -36,12 +41,22 @@ var ConfigSubpathCmd = &cobra.Command{ RunE: configSubpathCmdF, } +var ConfigGetCmd = &cobra.Command{ + Use: "get", + Short: "Get config setting", + Long: "Gets the value of a config setting by its name in dot notation.", + Example: `config get SqlSettings.DriverName`, + Args: cobra.ExactArgs(1), + RunE: configGetCmdF, +} + func init() { ConfigSubpathCmd.Flags().String("path", "", "Optional subpath; defaults to value in SiteURL") ConfigCmd.AddCommand( ValidateConfigCmd, ConfigSubpathCmd, + ConfigGetCmd, ) RootCmd.AddCommand(ConfigCmd) } @@ -102,3 +117,120 @@ func configSubpathCmdF(command *cobra.Command, args []string) error { return nil } + +func configGetCmdF(command *cobra.Command, args []string) error { + app, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer app.Shutdown() + + // create the model for config + // Note: app.Config() returns a pointer, make appropriate changes + config := app.Config() + + // get the print config setting and any error if there is + out, err := printConfigValues(configToMap(*config), strings.Split(args[0], "."), args[0]) + if err != nil { + return err + } + + fmt.Printf("%s", out) + + return nil +} + +// printConfigValues function prints out the value of the configSettings working recursively or +// gives an error if config setting is not in the file. +func printConfigValues(configMap map[string]interface{}, configSetting []string, name string) (string, error) { + + res, ok := configMap[configSetting[0]] + if !ok { + return "", fmt.Errorf("%s configuration setting is not in the file", name) + } + value := reflect.ValueOf(res) + switch value.Kind() { + case reflect.Map: + if len(configSetting) == 1 { + return printMap(value, 0), nil + } + return printConfigValues(res.(map[string]interface{}), configSetting[1:], name) + default: + if len(configSetting) == 1 { + return fmt.Sprintf("%s: \"%v\"\n", name, res), nil + } + return "", fmt.Errorf("%s configuration setting is not in the file", name) + } +} + +// printMap takes a reflect.Value and return a string, recursively if its a map with the given tab settings. +func printMap(value reflect.Value, tabVal int) string { + + // our output buffer + out := &bytes.Buffer{} + + for _, key := range value.MapKeys() { + val := value.MapIndex(key) + if newVal, ok := val.Interface().(map[string]interface{}); !ok { + fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal)) + fmt.Fprintf(out, "%v: \"%v\"\n", key.Interface(), val.Interface()) + } else { + fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal)) + fmt.Fprintf(out, "%v:\n", key.Interface()) + // going one level in, increase the tab + tabVal++ + fmt.Fprintf(out, "%s", printMap(reflect.ValueOf(newVal), tabVal)) + // coming back one level, decrease the tab + tabVal-- + } + } + + return out.String() + +} + +// configToMap converts our config into a map +func configToMap(s interface{}) map[string]interface{} { + return structToMap(s) +} + +// structToMap converts a struct into a map +func structToMap(t interface{}) map[string]interface{} { + defer func() { + if r := recover(); r != nil { + mlog.Error(fmt.Sprintf("Panicked in structToMap. This should never happen. %v", r)) + } + }() + + val := reflect.ValueOf(t) + + if val.Kind() != reflect.Struct { + return nil + } + + out := map[string]interface{}{} + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + + var value interface{} + + switch field.Kind() { + case reflect.Struct: + value = structToMap(field.Interface()) + case reflect.Ptr: + indirectType := field.Elem() + + if indirectType.Kind() == reflect.Struct { + value = structToMap(indirectType.Interface()) + } else { + value = indirectType.Interface() + } + default: + value = field.Interface() + } + + out[val.Type().Field(i).Name] = value + } + return out +} diff --git a/cmd/mattermost/commands/config_test.go b/cmd/mattermost/commands/config_test.go index fcc35bd02..a68818201 100644 --- a/cmd/mattermost/commands/config_test.go +++ b/cmd/mattermost/commands/config_test.go @@ -7,6 +7,9 @@ import ( "io/ioutil" "os" "path/filepath" + "reflect" + "sort" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -15,6 +18,42 @@ import ( "github.com/mattermost/mattermost-server/model" ) +type TestConfig struct { + TestServiceSettings TestServiceSettings + TestTeamSettings TestTeamSettings + TestClientRequirements TestClientRequirements + TestMessageExportSettings TestMessageExportSettings +} + +type TestMessageExportSettings struct { + Enableexport bool + Exportformat string + TestGlobalRelaySettings TestGlobalRelaySettings +} + +type TestGlobalRelaySettings struct { + Customertype string + Smtpusername string + Smtppassword string +} + +type TestServiceSettings struct { + Siteurl string + Websocketurl string + Licensedfieldlocation string +} + +type TestTeamSettings struct { + Sitename string + Maxuserperteam int +} + +type TestClientRequirements struct { + Androidlatestversion string + Androidminversion string + Desktoplatestversion string +} + func TestConfigValidate(t *testing.T) { dir, err := ioutil.TempDir("", "") require.NoError(t, err) @@ -28,3 +67,339 @@ func TestConfigValidate(t *testing.T) { assert.Error(t, RunCommand(t, "--config", "foo.json", "config", "validate")) assert.NoError(t, RunCommand(t, "--config", path, "config", "validate")) } + +func TestConfigGet(t *testing.T) { + // Error when no arguments are given + assert.Error(t, RunCommand(t, "config", "get")) + + // Error when more than one config settings are given + assert.Error(t, RunCommand(t, "config", "get", "abc", "def")) + + // Error when a config setting which is not in the config.json is given + assert.Error(t, RunCommand(t, "config", "get", "abc")) + + // No Error when a config setting which is in the config.json is given + assert.NoError(t, RunCommand(t, "config", "get", "MessageExportSettings")) + assert.NoError(t, RunCommand(t, "config", "get", "MessageExportSettings.GlobalRelaySettings")) + assert.NoError(t, RunCommand(t, "config", "get", "MessageExportSettings.GlobalRelaySettings.CustomerType")) + + // check output + output := CheckCommand(t, "config", "get", "MessageExportSettings") + + assert.Contains(t, string(output), "EnableExport") + assert.Contains(t, string(output), "ExportFormat") + assert.Contains(t, string(output), "DailyRunTime") + assert.Contains(t, string(output), "ExportFromTimestamp") +} + +func TestStructToMap(t *testing.T) { + + cases := []struct { + Name string + Input interface{} + Expected map[string]interface{} + }{ + { + Name: "Struct with one string field", + Input: struct { + Test string + }{ + Test: "test", + }, + Expected: map[string]interface{}{ + "Test": "test", + }, + }, + { + Name: "String with multiple fields of different ", + Input: struct { + Test1 string + Test2 int + Test3 string + Test4 bool + }{ + Test1: "test1", + Test2: 21, + Test3: "test2", + Test4: false, + }, + Expected: map[string]interface{}{ + "Test1": "test1", + "Test2": 21, + "Test3": "test2", + "Test4": false, + }, + }, + { + Name: "Nested fields", + Input: TestConfig{ + TestServiceSettings{"abc", "def", "ghi"}, + TestTeamSettings{"abc", 1}, + TestClientRequirements{"abc", "def", "ghi"}, + TestMessageExportSettings{true, "abc", TestGlobalRelaySettings{"abc", "def", "ghi"}}, + }, + Expected: map[string]interface{}{ + "TestServiceSettings": map[string]interface{}{ + "Siteurl": "abc", + "Websocketurl": "def", + "Licensedfieldlocation": "ghi", + }, + "TestTeamSettings": map[string]interface{}{ + "Sitename": "abc", + "Maxuserperteam": 1, + }, + "TestClientRequirements": map[string]interface{}{ + "Androidlatestversion": "abc", + "Androidminversion": "def", + "Desktoplatestversion": "ghi", + }, + "TestMessageExportSettings": map[string]interface{}{ + "Enableexport": true, + "Exportformat": "abc", + "TestGlobalRelaySettings": map[string]interface{}{ + "Customertype": "abc", + "Smtpusername": "def", + "Smtppassword": "ghi", + }, + }, + }, + }, + } + + for _, test := range cases { + t.Run(test.Name, func(t *testing.T) { + res := structToMap(test.Input) + + if !reflect.DeepEqual(res, test.Expected) { + t.Errorf("got %v want %v ", res, test.Expected) + } + }) + } + +} + +func TestConfigToMap(t *testing.T) { + // This test is almost the same as TestMapToStruct, but I have it here for the sake of completions + cases := []struct { + Name string + Input interface{} + Expected map[string]interface{} + }{ + { + Name: "Struct with one string field", + Input: struct { + Test string + }{ + Test: "test", + }, + Expected: map[string]interface{}{ + "Test": "test", + }, + }, + { + Name: "String with multiple fields of different ", + Input: struct { + Test1 string + Test2 int + Test3 string + Test4 bool + }{ + Test1: "test1", + Test2: 21, + Test3: "test2", + Test4: false, + }, + Expected: map[string]interface{}{ + "Test1": "test1", + "Test2": 21, + "Test3": "test2", + "Test4": false, + }, + }, + { + Name: "Nested fields", + Input: TestConfig{ + TestServiceSettings{"abc", "def", "ghi"}, + TestTeamSettings{"abc", 1}, + TestClientRequirements{"abc", "def", "ghi"}, + TestMessageExportSettings{true, "abc", TestGlobalRelaySettings{"abc", "def", "ghi"}}, + }, + Expected: map[string]interface{}{ + "TestServiceSettings": map[string]interface{}{ + "Siteurl": "abc", + "Websocketurl": "def", + "Licensedfieldlocation": "ghi", + }, + "TestTeamSettings": map[string]interface{}{ + "Sitename": "abc", + "Maxuserperteam": 1, + }, + "TestClientRequirements": map[string]interface{}{ + "Androidlatestversion": "abc", + "Androidminversion": "def", + "Desktoplatestversion": "ghi", + }, + "TestMessageExportSettings": map[string]interface{}{ + "Enableexport": true, + "Exportformat": "abc", + "TestGlobalRelaySettings": map[string]interface{}{ + "Customertype": "abc", + "Smtpusername": "def", + "Smtppassword": "ghi", + }, + }, + }, + }, + } + + for _, test := range cases { + t.Run(test.Name, func(t *testing.T) { + res := configToMap(test.Input) + + if !reflect.DeepEqual(res, test.Expected) { + t.Errorf("got %v want %v ", res, test.Expected) + } + }) + } +} + +func TestPrintMap(t *testing.T) { + + inputCases := []interface{}{ + map[string]interface{}{ + "CustomerType": "A9", + "SmtpUsername": "", + "SmtpPassword": "", + "EmailAddress": "", + }, + map[string]interface{}{ + "EnableExport": false, + "ExportFormat": "actiance", + "DailyRunTime": "01:00", + "GlobalRelaySettings": map[string]interface{}{ + "CustomerType": "A9", + "SmtpUsername": "", + "SmtpPassword": "", + "EmailAddress": "", + }, + }, + } + + outputCases := []string{ + "CustomerType: \"A9\"\nSmtpUsername: \"\"\nSmtpPassword: \"\"\nEmailAddress: \"\"\n", + "EnableExport: \"false\"\nExportFormat: \"actiance\"\nDailyRunTime: \"01:00\"\nGlobalRelaySettings:\n\t CustomerType: \"A9\"\n\tSmtpUsername: \"\"\n\tSmtpPassword: \"\"\n\tEmailAddress: \"\"\n", + } + + cases := []struct { + Name string + Input reflect.Value + Expected string + }{ + { + Name: "Basic print", + Input: reflect.ValueOf(inputCases[0]), + Expected: outputCases[0], + }, + { + Name: "Complex print", + Input: reflect.ValueOf(inputCases[1]), + Expected: outputCases[1], + }, + } + + for _, test := range cases { + t.Run(test.Name, func(t *testing.T) { + res := printMap(test.Input, 0) + + // create two slice of string formed by splitting our strings on \n + slice1 := strings.Split(res, "\n") + slice2 := strings.Split(res, "\n") + + sort.Strings(slice1) + sort.Strings(slice2) + + if !reflect.DeepEqual(slice1, slice2) { + t.Errorf("got '%#v' want '%#v", slice1, slice2) + } + + }) + } +} + +func TestPrintConfigValues(t *testing.T) { + + outputs := []string{ + "Siteurl: \"abc\"\nWebsocketurl: \"def\"\nLicensedfieldlocation: \"ghi\"\n", + "Sitename: \"abc\"\nMaxuserperteam: \"1\"\n", + "Androidlatestversion: \"abc\"\nAndroidminversion: \"def\"\nDesktoplatestversion: \"ghi\"\n", + "Enableexport: \"true\"\nExportformat: \"abc\"\nTestGlobalRelaySettings:\n\tCustomertype: \"abc\"\n\tSmtpusername: \"def\"\n\tSmtppassword: \"ghi\"\n", + "Customertype: \"abc\"\nSmtpusername: \"def\"\nSmtppassword: \"ghi\"\n", + } + + commands := []string{ + "TestServiceSettings", + "TestTeamSettings", + "TestClientRequirements", + "TestMessageExportSettings", + "TestMessageExportSettings.TestGlobalRelaySettings", + } + + input := TestConfig{ + TestServiceSettings{"abc", "def", "ghi"}, + TestTeamSettings{"abc", 1}, + TestClientRequirements{"abc", "def", "ghi"}, + TestMessageExportSettings{true, "abc", TestGlobalRelaySettings{"abc", "def", "ghi"}}, + } + + configMap := structToMap(input) + + cases := []struct { + Name string + Command string + Expected string + }{ + { + Name: "First test", + Command: commands[0], + Expected: outputs[0], + }, + { + Name: "Second test", + Command: commands[1], + Expected: outputs[1], + }, + { + Name: "third test", + Command: commands[2], + Expected: outputs[2], + }, + { + Name: "fourth test", + Command: commands[3], + Expected: outputs[3], + }, + { + Name: "fifth test", + Command: commands[4], + Expected: outputs[4], + }, + } + + for _, test := range cases { + t.Run(test.Name, func(t *testing.T) { + res, _ := printConfigValues(configMap, strings.Split(test.Command, "."), test.Command) + + // create two slice of string formed by splitting our strings on \n + slice1 := strings.Split(res, "\n") + slice2 := strings.Split(test.Expected, "\n") + + sort.Strings(slice1) + sort.Strings(slice2) + + if !reflect.DeepEqual(slice1, slice2) { + t.Errorf("got '%#v' want '%#v", slice1, slice2) + } + }) + } + +} -- cgit v1.2.3-1-g7c22