From ea83f988e995c2a3d447d50c3b66d2eceb7c7c42 Mon Sep 17 00:00:00 2001 From: Jan Semmelink <jan@uafrica.com> Date: Thu, 11 Nov 2021 10:50:23 +0200 Subject: [PATCH] Update config to allow load from ENV and override from REDIS for dynamic values --- config/struct.go | 43 +++++++++++++++++++++++++- config/struct_test.go | 4 +-- redis/redis.go | 34 ++++++++++++++++---- struct_utils/named_values_to_struct.go | 40 ++++++++++++++---------- 4 files changed, 96 insertions(+), 25 deletions(-) diff --git a/config/struct.go b/config/struct.go index 84053ad..04bfb88 100644 --- a/config/struct.go +++ b/config/struct.go @@ -5,6 +5,7 @@ import ( "gitlab.com/uafrica/go-utils/errors" "gitlab.com/uafrica/go-utils/logger" + "gitlab.com/uafrica/go-utils/string_utils" "gitlab.com/uafrica/go-utils/struct_utils" ) @@ -12,7 +13,47 @@ var ( prefixStructs = map[string]interface{}{} ) -func Load(prefix string, configStructPtr interface{}) error { +func LoadEnv(prefix string, configStructPtr interface{}) error { + return Load(prefix, configStructPtr, string_utils.EnvironmentKeyReader()) +} + +func Load(prefix string, configStructPtr interface{}, keyReader string_utils.KeyReader) error { + if !prefixRegex.MatchString(prefix) { + return errors.Errorf("config(%s) invalid prefix", prefix) + } + + //store before load in case it fails to be still part of docs + prefixStructs[prefix] = configStructPtr + + //read os.Getenv() or other reader... + nv := struct_utils.NamedValuesFromReader(prefix, keyReader) + logger.Debugf("nv: %+v", nv) + + //parse into struct + unused, err := struct_utils.UnmarshalNamedValues(nv, configStructPtr) + if err != nil { + return errors.Wrapf(err, "config(%s) cannot load", prefix) + } + if len(unused) > 0 { + //we still use os.Getenv() elsewhere, so some variables may not be in the struct + //e.g. AUDIT_QUEUE_URL is read from queues/sqs/producer which match config(prefix="AUDIT") + //so we cannot yet fail here, which we should, because config setting not used is often + //a reason for errors, when we try to configure something, then it does not work, and + //we cannot figure out why, but the value we did set, might just be misspelled etc. + //so, for now - do not fail here, just report the unused values + logger.Warnf("Note unused env (might be used elsewhere) for config(%s): %+v", prefix, unused) + //return errors.Errorf("config(%s): unknown %+v", prefix, unused) + } + + if validator, ok := configStructPtr.(Validator); ok { + if err := validator.Validate(); err != nil { + return errors.Wrapf(err, "config(%s) is invalid", prefix) + } + } + return nil +} + +func LoadRedis(prefix string, configStructPtr interface{}) error { if !prefixRegex.MatchString(prefix) { return errors.Errorf("config(%s) invalid prefix", prefix) } diff --git a/config/struct_test.go b/config/struct_test.go index 587f8bd..297664f 100644 --- a/config/struct_test.go +++ b/config/struct_test.go @@ -37,7 +37,7 @@ func TestLoad(t *testing.T) { os.Setenv("TEST_VALUE_HOLIDAYS", "[2021-03-21,2021-04-27,2021-05-01,2021-06-16,2021-08-09,2021-12-16,2021-12-25]") c := Config{} - if err := config.Load("TEST_VALUE", &c); err != nil { + if err := config.LoadEnv("TEST_VALUE", &c); err != nil { t.Fatalf("Cannot load config: %+v", err) } t.Logf("Loaded config: %+v", c) @@ -135,7 +135,7 @@ func TestLogConfig(t *testing.T) { os.Setenv("LOG_OTHER", "1") os.Setenv("LOG_SEARCH_OTHER", "2") c := LogConfig{} - err := config.Load("LOG", &c) + err := config.LoadEnv("LOG", &c) if err != nil { t.Fatalf("Failed: %+v", err) } diff --git a/redis/redis.go b/redis/redis.go index 23d667e..5dac499 100644 --- a/redis/redis.go +++ b/redis/redis.go @@ -10,9 +10,11 @@ import ( "github.com/go-redis/redis/v8" "gitlab.com/uafrica/go-utils/errors" "gitlab.com/uafrica/go-utils/logger" + "gitlab.com/uafrica/go-utils/string_utils" ) type IRedis interface { + string_utils.KeyReader Del(key string) error SetJSON(key string, value interface{}) error SetJSONIndefinitely(key string, value interface{}) error @@ -21,7 +23,6 @@ type IRedis interface { SetString(key string, value string) error SetStringIndefinitely(key string, value string) error SetStringForDur(key string, value string, dur time.Duration) error - GetString(key string) (value string, ok bool) } type redisWithContext struct { @@ -67,14 +68,18 @@ func (r redisWithContext) SetJSONForDur(key string, value interface{}, dur time. if r.client == nil { return errors.Errorf("REDIS disabled: cannot set JSON key(%s) = (%T)%v", key, value, value) } - jsonBytes, err := json.Marshal(value) - if err != nil { - return errors.Wrapf(err, "failed to JSON encode key(%s) = (%T)", key, value) + valueStr, ok := value.(string) + if !ok { + jsonBytes, err := json.Marshal(value) + if err != nil { + return errors.Wrapf(err, "failed to JSON encode key(%s) = (%T)", key, value) + } + valueStr = string(jsonBytes) } - if _, err = r.client.Set(r.Context, key, string(jsonBytes), dur).Result(); err != nil { + if _, err := r.client.Set(r.Context, key, valueStr, dur).Result(); err != nil { return errors.Wrapf(err, "failed to set JSON key(%s)", key) } - logger.Debugf("REDIS.SetJSON(%s)=%s (%T) (exp: %v)", key, string(jsonBytes), value, dur) + logger.Debugf("REDIS.SetJSON(%s)=%s (%T) (exp: %v)", key, valueStr, value, dur) return nil } @@ -130,6 +135,23 @@ func (r redisWithContext) GetString(key string) (string, bool) { return value, true } +func (r redisWithContext) Keys(prefix string) []string { + if r.client == nil { + return nil + } + value, err := r.client.Keys(r.Context, prefix+"*").Result() + if err != nil { /* Actual error */ + if err != redis.Nil { /* other than no keys match */ + logger.Errorf("Error fetching redis keys(%s*): %+v", prefix, err) + } else { + logger.Errorf("Failed: %+v", err) + } + return nil //no matches + } + logger.Debugf("Keys(%s): %+v", prefix, value) + return value +} + //global connection to REDIS used in all context var globalClient *redis.Client diff --git a/struct_utils/named_values_to_struct.go b/struct_utils/named_values_to_struct.go index 2f118e4..81c5836 100644 --- a/struct_utils/named_values_to_struct.go +++ b/struct_utils/named_values_to_struct.go @@ -3,13 +3,14 @@ package struct_utils import ( "encoding/csv" "encoding/json" - "os" "reflect" "sort" "strconv" "strings" "gitlab.com/uafrica/go-utils/errors" + "gitlab.com/uafrica/go-utils/logger" + "gitlab.com/uafrica/go-utils/string_utils" ) //Purpose: @@ -34,24 +35,31 @@ import ( // // MY_LIB_CONFIG_ADDRS=["55 Crescent, Town", "12 Big Street, City"] -> 2 values including commas because of quoted CSV func NamedValuesFromEnv(prefix string) map[string][]string { + return NamedValuesFromReader(prefix, string_utils.EnvironmentKeyReader()) +} + +func NamedValuesFromReader(prefix string, reader string_utils.KeyReader) map[string][]string { + if reader == nil { + return nil + } result := map[string][]string{} prefix += "_" - for _, env := range os.Environ() { - if strings.HasPrefix(env, prefix) { - parts := strings.SplitN(env[len(prefix):], "=", 2) - if len(parts) == 2 { - //name = parts[0], value = parts[1] - value := parts[1] - result[strings.ToLower(parts[0])] = []string{value} + for _, key := range reader.Keys(prefix) { + value, ok := reader.GetString(key) + key = key[len(prefix):] + if !ok { + logger.Debugf("Key(%s) undefined", key) + continue + } + logger.Debugf("key(%s)=\"%s\"", key, value) + result[strings.ToLower(key)] = []string{value} - //split only if valid CSV between [...] - if value[0] == '[' && value[len(value)-1] == ']' { - csvReader := csv.NewReader(strings.NewReader(value[1 : len(value)-1])) - csvValues, csvErr := csvReader.Read() //this automatically removes quotes around some/all CSV inside the [...] - if csvErr == nil { - result[strings.ToLower(parts[0])] = csvValues - } - } + //split only if valid CSV between [...] + if value[0] == '[' && value[len(value)-1] == ']' { + csvReader := csv.NewReader(strings.NewReader(value[1 : len(value)-1])) + csvValues, csvErr := csvReader.Read() //this automatically removes quotes around some/all CSV inside the [...] + if csvErr == nil { + result[strings.ToLower(key)] = csvValues } } } -- GitLab