* 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
Этот коммит содержится в:
коммит произвёл
Harrison Healey
родитель
43bdbb0c3a
Коммит
9385dc750d
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user