From ff077c6761bd4b6d170831f7f2ba474c2a9bd5e0 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Fri, 6 Apr 2018 12:17:43 -0400 Subject: MM-8400 Provide default config values to viper so that it reads all environment variables (#8581) * MM-8400 Provide default config values to viper so that it reads all environment variables * Added unit tests --- utils/config.go | 93 ++++++++++++++++++++++++++++++++++++++++++++----- utils/config_test.go | 98 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 150 insertions(+), 41 deletions(-) diff --git a/utils/config.go b/utils/config.go index 13295b362..b87f164ee 100644 --- a/utils/config.go +++ b/utils/config.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "os" "path/filepath" + "reflect" "strconv" "strings" @@ -212,15 +213,8 @@ func (w *ConfigWatcher) Close() { // ReadConfig reads and parses the given configuration. func ReadConfig(r io.Reader, allowEnvironmentOverrides bool) (*model.Config, error) { - v := viper.New() + v := newViper(allowEnvironmentOverrides) - if allowEnvironmentOverrides { - v.SetEnvPrefix("mm") - v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - v.AutomaticEnv() - } - - v.SetConfigType("json") if err := v.ReadConfig(r); err != nil { return nil, err } @@ -236,6 +230,89 @@ func ReadConfig(r io.Reader, allowEnvironmentOverrides bool) (*model.Config, err return &config, unmarshalErr } +func newViper(allowEnvironmentOverrides bool) *viper.Viper { + v := viper.New() + + v.SetConfigType("json") + + if allowEnvironmentOverrides { + v.SetEnvPrefix("mm") + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + v.AutomaticEnv() + } + + // Set zeroed defaults for all the config settings so that Viper knows what environment variables + // it needs to be looking for. The correct defaults will later be applied using Config.SetDefaults. + defaults := flattenStructToMap(structToMap(reflect.TypeOf(model.Config{}))) + + for key, value := range defaults { + v.SetDefault(key, value) + } + + return v +} + +// Converts a struct type into a nested map with keys matching the struct's fields and values +// matching the zeroed value of the corresponding field. +func structToMap(t reflect.Type) map[string]interface{} { + if t.Kind() != reflect.Struct { + // Should never hit this, but this will prevent a panic if that does happen somehow + return nil + } + + out := make(map[string]interface{}) + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + var value interface{} + + switch field.Type.Kind() { + case reflect.Struct: + value = structToMap(field.Type) + case reflect.Ptr: + value = nil + default: + value = reflect.Zero(field.Type).Interface() + } + + out[field.Name] = value + } + + return out +} + +// Flattens a nested map so that the result is a single map with keys corresponding to the +// path through the original map. For example, +// { +// "a": { +// "b": 1 +// }, +// "c": "sea" +// } +// would flatten to +// { +// "a.b": 1, +// "c": "sea" +// } +func flattenStructToMap(in map[string]interface{}) map[string]interface{} { + out := make(map[string]interface{}) + + for key, value := range in { + if valueAsMap, ok := value.(map[string]interface{}); ok { + sub := flattenStructToMap(valueAsMap) + + for subKey, subValue := range sub { + out[key+"."+subKey] = subValue + } + } else { + out[key] = value + } + } + + return out +} + // ReadConfigFile reads and parses the configuration at the given file path. func ReadConfigFile(path string, allowEnvironmentOverrides bool) (*model.Config, error) { f, err := os.Open(path) diff --git a/utils/config_test.go b/utils/config_test.go index 84e7291b0..a998bfbc6 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -50,48 +50,80 @@ func TestFindConfigFile(t *testing.T) { } func TestConfigFromEnviroVars(t *testing.T) { - os.Setenv("MM_TEAMSETTINGS_SITENAME", "From Environment") - os.Setenv("MM_TEAMSETTINGS_CUSTOMBRANDTEXT", "Custom Brand") - os.Setenv("MM_SERVICESETTINGS_ENABLECOMMANDS", "false") - os.Setenv("MM_SERVICESETTINGS_READTIMEOUT", "400") - TranslationsPreInit() - cfg, cfgPath, err := LoadConfig("config.json") - require.Nil(t, err) - if cfg.TeamSettings.SiteName != "From Environment" { - t.Fatal("Couldn't read config from environment var") - } + config := `{ + "ServiceSettings": { + "EnableCommands": true, + "ReadTimeout": 100 + }, + "TeamSettings": { + "SiteName": "Mattermost", + "CustomBrandText": "" + } + }` - if *cfg.TeamSettings.CustomBrandText != "Custom Brand" { - t.Fatal("Couldn't read config from environment var") - } + t.Run("string settings", func(t *testing.T) { + os.Setenv("MM_TEAMSETTINGS_SITENAME", "From Environment") + os.Setenv("MM_TEAMSETTINGS_CUSTOMBRANDTEXT", "Custom Brand") - if *cfg.ServiceSettings.EnableCommands { - t.Fatal("Couldn't read config from environment var") - } + cfg, err := ReadConfig(strings.NewReader(config), true) + require.Nil(t, err) - if *cfg.ServiceSettings.ReadTimeout != 400 { - t.Fatal("Couldn't read config from environment var") - } + if cfg.TeamSettings.SiteName != "From Environment" { + t.Fatal("Couldn't read config from environment var") + } - os.Unsetenv("MM_TEAMSETTINGS_SITENAME") - os.Unsetenv("MM_TEAMSETTINGS_CUSTOMBRANDTEXT") - os.Unsetenv("MM_SERVICESETTINGS_ENABLECOMMANDS") - os.Unsetenv("MM_SERVICESETTINGS_READTIMEOUT") + if *cfg.TeamSettings.CustomBrandText != "Custom Brand" { + t.Fatal("Couldn't read config from environment var") + } - cfg.TeamSettings.SiteName = "Mattermost" - *cfg.ServiceSettings.SiteURL = "" - *cfg.ServiceSettings.EnableCommands = true - *cfg.ServiceSettings.ReadTimeout = 300 - SaveConfig(cfgPath, cfg) + os.Unsetenv("MM_TEAMSETTINGS_SITENAME") + os.Unsetenv("MM_TEAMSETTINGS_CUSTOMBRANDTEXT") - cfg, _, err = LoadConfig("config.json") - require.Nil(t, err) + cfg, err = ReadConfig(strings.NewReader(config), true) + require.Nil(t, err) - if cfg.TeamSettings.SiteName != "Mattermost" { - t.Fatal("should have been reset") - } + if cfg.TeamSettings.SiteName != "Mattermost" { + t.Fatal("should have been reset") + } + }) + + t.Run("boolean setting", func(t *testing.T) { + os.Setenv("MM_SERVICESETTINGS_ENABLECOMMANDS", "false") + defer os.Unsetenv("MM_SERVICESETTINGS_ENABLECOMMANDS") + + cfg, err := ReadConfig(strings.NewReader(config), true) + require.Nil(t, err) + + if *cfg.ServiceSettings.EnableCommands { + t.Fatal("Couldn't read config from environment var") + } + }) + + t.Run("integer setting", func(t *testing.T) { + os.Setenv("MM_SERVICESETTINGS_READTIMEOUT", "400") + defer os.Unsetenv("MM_SERVICESETTINGS_READTIMEOUT") + + cfg, err := ReadConfig(strings.NewReader(config), true) + require.Nil(t, err) + + if *cfg.ServiceSettings.ReadTimeout != 400 { + t.Fatal("Couldn't read config from environment var") + } + }) + + t.Run("setting missing from config.json", func(t *testing.T) { + os.Setenv("MM_SERVICESETTINGS_SITEURL", "https://example.com") + defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") + + cfg, err := ReadConfig(strings.NewReader(config), true) + require.Nil(t, err) + + if *cfg.ServiceSettings.SiteURL != "https://example.com" { + t.Fatal("Couldn't read config from environment var") + } + }) } func TestValidateLocales(t *testing.T) { -- cgit v1.2.3-1-g7c22