[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
Этот коммит содержится в:
Mukul Rawat
2018-10-15 20:19:25 +05:30
коммит произвёл Harrison Healey
родитель 43bdbb0c3a
Коммит 9385dc750d
2 изменённых файлов: 507 добавлений и 0 удалений

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

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

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

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