This commit is contained in:
Mike Schwörer 2023-01-06 02:02:22 +01:00
parent ba07625b7c
commit 02be696c25
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
2 changed files with 163 additions and 23 deletions

View File

@ -14,9 +14,21 @@ import (
// ApplyEnvOverrides overrides field values from environment variables // ApplyEnvOverrides overrides field values from environment variables
// //
// fields must be tagged with `env:"env_key"` // fields must be tagged with `env:"env_key"`
func ApplyEnvOverrides[T any](c *T) error { //
// only works on exported fields
//
// fields without an env tag are ignored
// fields with an `env:"-"` tag are ignore
//
// sub-structs are recursively parsed (if they have an env tag) and the env-variable keys are delimited by the delim parameter
// sub-structs with `env:""` are also parsed, but the delimited is skipped (they are handled as if they were one level higher)
func ApplyEnvOverrides[T any](c *T, delim string) error {
rval := reflect.ValueOf(c).Elem() rval := reflect.ValueOf(c).Elem()
return processEnvOverrides(rval, delim, "")
}
func processEnvOverrides(rval reflect.Value, delim string, prefix string) error {
rtyp := rval.Type() rtyp := rval.Type()
for i := 0; i < rtyp.NumField(); i++ { for i := 0; i < rtyp.NumField(); i++ {
@ -24,12 +36,36 @@ func ApplyEnvOverrides[T any](c *T) error {
rsfield := rtyp.Field(i) rsfield := rtyp.Field(i)
rvfield := rval.Field(i) rvfield := rval.Field(i)
envkey := rsfield.Tag.Get("env") if !rsfield.IsExported() {
if envkey == "" {
continue continue
} }
envval, efound := os.LookupEnv(envkey) if rvfield.Kind() == reflect.Struct {
envkey, found := rsfield.Tag.Lookup("env")
if !found || envkey == "-" {
continue
}
subPrefix := prefix
if envkey != "" {
subPrefix = subPrefix + envkey + delim
}
err := processEnvOverrides(rvfield, delim, subPrefix)
if err != nil {
return err
}
}
envkey := rsfield.Tag.Get("env")
if envkey == "" || envkey == "-" {
continue
}
fullEnvKey := prefix + envkey
envval, efound := os.LookupEnv(fullEnvKey)
if !efound { if !efound {
continue continue
} }
@ -38,86 +74,86 @@ func ApplyEnvOverrides[T any](c *T) error {
rvfield.Set(reflect.ValueOf(envval)) rvfield.Set(reflect.ValueOf(envval))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, envval) fmt.Printf("[CONF] Overwrite config '%s' () with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(int(0)) { } else if rvfield.Type() == reflect.TypeOf(int(0)) {
envint, err := strconv.ParseInt(envval, 10, bits.UintSize) envint, err := strconv.ParseInt(envval, 10, bits.UintSize)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int (value := '%s')", envkey, envval)) return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int (value := '%s')", fullEnvKey, envval))
} }
rvfield.Set(reflect.ValueOf(int(envint))) rvfield.Set(reflect.ValueOf(int(envint)))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, envval) fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(int64(0)) { } else if rvfield.Type() == reflect.TypeOf(int64(0)) {
envint, err := strconv.ParseInt(envval, 10, 64) envint, err := strconv.ParseInt(envval, 10, 64)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int64 (value := '%s')", envkey, envval)) return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int64 (value := '%s')", fullEnvKey, envval))
} }
rvfield.Set(reflect.ValueOf(int64(envint))) rvfield.Set(reflect.ValueOf(int64(envint)))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, envval) fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(int32(0)) { } else if rvfield.Type() == reflect.TypeOf(int32(0)) {
envint, err := strconv.ParseInt(envval, 10, 32) envint, err := strconv.ParseInt(envval, 10, 32)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", envkey, envval)) return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", fullEnvKey, envval))
} }
rvfield.Set(reflect.ValueOf(int32(envint))) rvfield.Set(reflect.ValueOf(int32(envint)))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, envval) fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(int8(0)) { } else if rvfield.Type() == reflect.TypeOf(int8(0)) {
envint, err := strconv.ParseInt(envval, 10, 8) envint, err := strconv.ParseInt(envval, 10, 8)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", envkey, envval)) return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", fullEnvKey, envval))
} }
rvfield.Set(reflect.ValueOf(int8(envint))) rvfield.Set(reflect.ValueOf(int8(envint)))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, envval) fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(time.Duration(0)) { } else if rvfield.Type() == reflect.TypeOf(time.Duration(0)) {
dur, err := timeext.ParseDurationShortString(envval) dur, err := timeext.ParseDurationShortString(envval)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to duration (value := '%s')", envkey, envval)) return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to duration (value := '%s')", fullEnvKey, envval))
} }
rvfield.Set(reflect.ValueOf(dur)) rvfield.Set(reflect.ValueOf(dur))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, dur.String()) fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, dur.String())
} else if rvfield.Type() == reflect.TypeOf(time.UnixMilli(0)) { } else if rvfield.Type() == reflect.TypeOf(time.UnixMilli(0)) {
tim, err := time.Parse(time.RFC3339Nano, envval) tim, err := time.Parse(time.RFC3339Nano, envval)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to time.time (value := '%s')", envkey, envval)) return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to time.time (value := '%s')", fullEnvKey, envval))
} }
rvfield.Set(reflect.ValueOf(tim)) rvfield.Set(reflect.ValueOf(tim))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, tim.String()) fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, tim.String())
} else if rvfield.Type().ConvertibleTo(reflect.TypeOf(int(0))) { } else if rvfield.Type().ConvertibleTo(reflect.TypeOf(int(0))) {
envint, err := strconv.ParseInt(envval, 10, 8) envint, err := strconv.ParseInt(envval, 10, 8)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to <%s, ,int> (value := '%s')", rvfield.Type().Name(), envkey, envval)) return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to <%s, ,int> (value := '%s')", rvfield.Type().Name(), fullEnvKey, envval))
} }
envcvl := reflect.ValueOf(envint).Convert(rvfield.Type()) envcvl := reflect.ValueOf(envint).Convert(rvfield.Type())
rvfield.Set(envcvl) rvfield.Set(envcvl)
fmt.Printf("[CONF] Overwrite config '%s' with '%v'\n", envkey, envcvl.Interface()) fmt.Printf("[CONF] Overwrite config '%s' with '%v'\n", fullEnvKey, envcvl.Interface())
} else if rvfield.Type().ConvertibleTo(reflect.TypeOf("")) { } else if rvfield.Type().ConvertibleTo(reflect.TypeOf("")) {
@ -125,7 +161,7 @@ func ApplyEnvOverrides[T any](c *T) error {
rvfield.Set(envcvl) rvfield.Set(envcvl)
fmt.Printf("[CONF] Overwrite config '%s' with '%v'\n", envkey, envcvl.Interface()) fmt.Printf("[CONF] Overwrite config '%s' with '%v'\n", fullEnvKey, envcvl.Interface())
} else { } else {
return errors.New(fmt.Sprintf("Unknown kind/type in config: [ %s | %s ]", rvfield.Kind().String(), rvfield.Type().String())) return errors.New(fmt.Sprintf("Unknown kind/type in config: [ %s | %s ]", rvfield.Kind().String(), rvfield.Type().String()))

View File

@ -1,6 +1,7 @@
package confext package confext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"testing" "testing"
"time" "time"
) )
@ -40,7 +41,7 @@ func TestApplyEnvOverridesNoop(t *testing.T) {
output := input output := input
err := ApplyEnvOverrides(&output) err := ApplyEnvOverrides(&output, ".")
if err != nil { if err != nil {
t.Errorf("%v", err) t.Errorf("%v", err)
t.FailNow() t.FailNow()
@ -92,7 +93,7 @@ func TestApplyEnvOverridesSimple(t *testing.T) {
t.Setenv("TEST_V8", "1min4s") t.Setenv("TEST_V8", "1min4s")
t.Setenv("TEST_V9", "2009-11-10T23:00:00Z") t.Setenv("TEST_V9", "2009-11-10T23:00:00Z")
err := ApplyEnvOverrides(&data) err := ApplyEnvOverrides(&data, ".")
if err != nil { if err != nil {
t.Errorf("%v", err) t.Errorf("%v", err)
t.FailNow() t.FailNow()
@ -109,6 +110,109 @@ func TestApplyEnvOverridesSimple(t *testing.T) {
assertEqual(t, data.V9, time.Unix(1257894000, 0).UTC()) assertEqual(t, data.V9, time.Unix(1257894000, 0).UTC())
} }
func TestApplyEnvOverridesRecursive(t *testing.T) {
type subdata struct {
V1 int `env:"SUB_V1"`
VX string ``
V2 string `env:"SUB_V2"`
V8 time.Duration `env:"SUB_V3"`
V9 time.Time `env:"SUB_V4"`
}
type testdata struct {
V1 int `env:"TEST_V1"`
VX string ``
Sub1 subdata ``
Sub2 subdata `env:"TEST_V2"`
Sub3 subdata `env:"TEST_V3"`
Sub4 subdata `env:""`
V5 string `env:"-"`
}
data := testdata{
V1: 1,
VX: "2",
V5: "no",
Sub1: subdata{
V1: 3,
VX: "4",
V2: "5",
V8: 6 * time.Second,
V9: time.Date(2000, 1, 7, 1, 1, 1, 0, time.UTC),
},
Sub2: subdata{
V1: 8,
VX: "9",
V2: "10",
V8: 11 * time.Second,
V9: time.Date(2000, 1, 12, 1, 1, 1, 0, timeext.TimezoneBerlin),
},
Sub3: subdata{
V1: 13,
VX: "14",
V2: "15",
V8: 16 * time.Second,
V9: time.Date(2000, 1, 17, 1, 1, 1, 0, timeext.TimezoneBerlin),
},
Sub4: subdata{
V1: 18,
VX: "19",
V2: "20",
V8: 21 * time.Second,
V9: time.Date(2000, 1, 22, 1, 1, 1, 0, timeext.TimezoneBerlin),
},
}
t.Setenv("TEST_V1", "999")
t.Setenv("-", "yes")
t.Setenv("TEST_V2_SUB_V1", "846")
t.Setenv("TEST_V2_SUB_V2", "222_hello_world")
t.Setenv("TEST_V2_SUB_V3", "1min4s")
t.Setenv("TEST_V2_SUB_V4", "2009-11-10T23:00:00Z")
t.Setenv("TEST_V3_SUB_V1", "33846")
t.Setenv("TEST_V3_SUB_V2", "33_hello_world")
t.Setenv("TEST_V3_SUB_V3", "33min4s")
t.Setenv("TEST_V3_SUB_V4", "2033-11-10T23:00:00Z")
t.Setenv("SUB_V1", "11")
t.Setenv("SUB_V2", "22")
t.Setenv("SUB_V3", "33min")
t.Setenv("SUB_V4", "2044-01-01T00:00:00Z")
err := ApplyEnvOverrides(&data, "_")
if err != nil {
t.Errorf("%v", err)
t.FailNow()
}
assertEqual(t, data.V1, 999)
assertEqual(t, data.VX, "2")
assertEqual(t, data.V5, "no")
assertEqual(t, data.Sub1.V1, 3)
assertEqual(t, data.Sub1.VX, "4")
assertEqual(t, data.Sub1.V2, "5")
assertEqual(t, data.Sub1.V8, time.Second*6)
assertEqual(t, data.Sub1.V9, time.Unix(947206861, 0).UTC())
assertEqual(t, data.Sub2.V1, 846)
assertEqual(t, data.Sub2.VX, "9")
assertEqual(t, data.Sub2.V2, "222_hello_world")
assertEqual(t, data.Sub2.V8, time.Second*64)
assertEqual(t, data.Sub2.V9, time.Unix(1257894000, 0).UTC())
assertEqual(t, data.Sub3.V1, 33846)
assertEqual(t, data.Sub3.VX, "14")
assertEqual(t, data.Sub3.V2, "33_hello_world")
assertEqual(t, data.Sub3.V8, time.Second*1984)
assertEqual(t, data.Sub3.V9, time.Unix(2015276400, 0).UTC())
assertEqual(t, data.Sub4.V1, 11)
assertEqual(t, data.Sub4.VX, "19")
assertEqual(t, data.Sub4.V2, "22")
assertEqual(t, data.Sub4.V8, time.Second*1980)
assertEqual(t, data.Sub4.V9, time.Unix(2335219200, 0).UTC())
}
func assertEqual[T comparable](t *testing.T, actual T, expected T) { func assertEqual[T comparable](t *testing.T, actual T, expected T) {
if actual != expected { if actual != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected) t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)