summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMukul Rawat <mukulsrawat@gmail.com>2018-10-15 20:19:25 +0530
committerHarrison Healey <harrisonmhealey@gmail.com>2018-10-15 10:49:25 -0400
commit9385dc750d59a4dcac168cbdba926892a467ad1d (patch)
tree54161870ac48d09e6bf31a8dd301071eb0210eaa
parent43bdbb0c3a0d93a8bf34ae5e2abbad4586a2581b (diff)
downloadchat-9385dc750d59a4dcac168cbdba926892a467ad1d.tar.gz
chat-9385dc750d59a4dcac168cbdba926892a467ad1d.tar.bz2
chat-9385dc750d59a4dcac168cbdba926892a467ad1d.zip
[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
-rw-r--r--cmd/mattermost/commands/config.go132
-rw-r--r--cmd/mattermost/commands/config_test.go375
2 files changed, 507 insertions, 0 deletions
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)
+ }
+ })
+ }
+
+}