fix crash when setting deprecated settings with environment (#2529) (#2550)

This commit is contained in:
Alessandro Ros 2023-10-23 20:07:28 +02:00 committed by GitHub
parent f237a1dc28
commit 4dc6e338dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 168 additions and 101 deletions

View File

@ -28,8 +28,14 @@ func loadEnvInternal(env map[string]string, prefix string, prv reflect.Value) er
return loadEnvInternal(env, prefix, prv.Addr())
}
rt := prv.Type().Elem()
if i, ok := prv.Interface().(Unmarshaler); ok {
if ev, ok := env[prefix]; ok {
if prv.IsNil() {
prv.Set(reflect.New(rt))
i = prv.Interface().(Unmarshaler)
}
err := i.UnmarshalEnv(prefix, ev)
if err != nil {
return fmt.Errorf("%s: %s", prefix, err)
@ -43,8 +49,6 @@ func loadEnvInternal(env map[string]string, prefix string, prv reflect.Value) er
return nil
}
rt := prv.Type().Elem()
switch rt {
case reflect.TypeOf(""):
if ev, ok := env[prefix]; ok {
@ -173,12 +177,12 @@ func loadEnvInternal(env map[string]string, prefix string, prv reflect.Value) er
switch {
case rt.Elem() == reflect.TypeOf(""):
if ev, ok := env[prefix]; ok {
if prv.IsNil() {
prv.Set(reflect.New(rt))
}
if ev == "" {
prv.Elem().Set(reflect.MakeSlice(prv.Elem().Type(), 0, 0))
} else {
if prv.IsNil() {
prv.Set(reflect.New(rt))
}
prv.Elem().Set(reflect.ValueOf(strings.Split(ev, ",")))
}
}
@ -186,9 +190,6 @@ func loadEnvInternal(env map[string]string, prefix string, prv reflect.Value) er
case rt.Elem().Kind() == reflect.Struct:
if ev, ok := env[prefix]; ok && ev == "" { // special case: empty list
if prv.IsNil() {
prv.Set(reflect.New(rt))
}
prv.Elem().Set(reflect.MakeSlice(prv.Elem().Type(), 0, 0))
} else {
for i := 0; ; i++ {
@ -213,13 +214,20 @@ func loadEnvInternal(env map[string]string, prefix string, prv reflect.Value) er
return fmt.Errorf("unsupported type: %v", rt)
}
// Load loads the configuration from the environment.
func Load(prefix string, v interface{}) error {
func loadWithEnv(env map[string]string, prefix string, v interface{}) error {
return loadEnvInternal(env, prefix, reflect.ValueOf(v).Elem())
}
func envToMap() map[string]string {
env := make(map[string]string)
for _, kv := range os.Environ() {
tmp := strings.SplitN(kv, "=", 2)
env[tmp[0]] = tmp[1]
}
return loadEnvInternal(env, prefix, reflect.ValueOf(v).Elem())
return env
}
// Load loads the configuration from the environment.
func Load(prefix string, v interface{}) error {
return loadWithEnv(envToMap(), prefix, v)
}

View File

@ -9,13 +9,28 @@ import (
"github.com/stretchr/testify/require"
)
type subStruct struct {
MyParam int `json:"myParam"`
func stringPtr(v string) *string {
return &v
}
type mapEntry struct {
MyValue string `json:"myValue"`
MyStruct subStruct `json:"myStruct"`
func intPtr(v int) *int {
return &v
}
func uint64Ptr(v uint64) *uint64 {
return &v
}
func boolPtr(v bool) *bool {
return &v
}
func float64Ptr(v float64) *float64 {
return &v
}
func durationPtr(v time.Duration) *time.Duration {
return &v
}
type myDuration time.Duration
@ -40,106 +55,150 @@ func (d *myDuration) UnmarshalEnv(_ string, v string) error {
return d.UnmarshalJSON([]byte(`"` + v + `"`))
}
type subStruct struct {
MyParam int `json:"myParam"`
}
type mapEntry struct {
MyValue string `json:"myValue"`
MyStruct subStruct `json:"myStruct"`
}
type mySubStruct struct {
URL string `json:"url"`
Username string `json:"username"`
Password string `json:"password"`
MyInt2 int `json:"myInt2"`
}
type testStruct struct {
MyString string `json:"myString"`
MyInt int `json:"myInt"`
MyFloat float64 `json:"myFloat"`
MyBool bool `json:"myBool"`
MyDuration myDuration `json:"myDuration"`
MyMap map[string]*mapEntry `json:"myMap"`
MySlice []string `json:"mySlice"`
MySliceEmpty []string `json:"mySliceEmpty"`
MySliceSubStruct []mySubStruct `json:"mySliceSubStruct"`
MySliceSubStructEmpty []mySubStruct `json:"mySliceSubStructEmpty"`
MyString string `json:"myString"`
MyStringOpt *string `json:"myStringOpt"`
MyInt int `json:"myInt"`
MyIntOpt *int `json:"myIntOpt"`
MyUint uint64 `json:"myUint"`
MyUintOpt *uint64 `json:"myUintOpt"`
MyFloat float64 `json:"myFloat"`
MyFloatOpt *float64 `json:"myFloatOpt"`
MyBool bool `json:"myBool"`
MyBoolOpt *bool `json:"myBoolOpt"`
MyDuration myDuration `json:"myDuration"`
MyDurationOpt *myDuration `json:"myDurationOpt"`
MyDurationOptUnset *myDuration `json:"myDurationOptUnset"`
MyMap map[string]*mapEntry `json:"myMap"`
MySliceString []string `json:"mySliceString"`
MySliceStringEmpty []string `json:"mySliceStringEmpty"`
MySliceStringOpt *[]string `json:"mySliceStringOpt"`
MySliceStringOptUnset *[]string `json:"mySliceStringOptUnset"`
MySliceSubStruct []mySubStruct `json:"mySliceSubStruct"`
MySliceSubStructEmpty []mySubStruct `json:"mySliceSubStructEmpty"`
MySliceSubStructOpt *[]mySubStruct `json:"mySliceSubStructOpt"`
MySliceSubStructOptUnset *[]mySubStruct `json:"mySliceSubStructOptUnset"`
Unset *bool `json:"unset"`
}
func TestLoad(t *testing.T) {
os.Setenv("MYPREFIX_MYSTRING", "testcontent")
defer os.Unsetenv("MYPREFIX_MYSTRING")
env := map[string]string{
"MYPREFIX_MYSTRING": "testcontent",
"MYPREFIX_MYSTRINGOPT": "testcontent2",
"MYPREFIX_MYINT": "123",
"MYPREFIX_MYINTOPT": "456",
"MYPREFIX_MYUINT": "8910",
"MYPREFIX_MYUINTOPT": "112313",
"MYPREFIX_MYFLOAT": "15.2",
"MYPREFIX_MYFLOATOPT": "16.2",
"MYPREFIX_MYBOOL": "yes",
"MYPREFIX_MYBOOLOPT": "false",
"MYPREFIX_MYDURATION": "22s",
"MYPREFIX_MYDURATIONOPT": "30s",
"MYPREFIX_MYMAP_MYKEY": "",
"MYPREFIX_MYMAP_MYKEY2_MYVALUE": "asd",
"MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM": "456",
"MYPREFIX_MYSLICESTRING": "val1,val2",
"MYPREFIX_MYSLICESTRINGEMPTY": "",
"MYPREFIX_MYSLICESTRINGOPT": "aa",
"MYPREFIX_MYSLICESUBSTRUCT_0_URL": "url1",
"MYPREFIX_MYSLICESUBSTRUCT_0_USERNAME": "user1",
"MYPREFIX_MYSLICESUBSTRUCT_0_PASSWORD": "pass1",
"MYPREFIX_MYSLICESUBSTRUCT_1_URL": "url2",
"MYPREFIX_MYSLICESUBSTRUCT_1_PASSWORD": "pass2",
"MYPREFIX_MYSLICESUBSTRUCTEMPTY": "",
"MYPREFIX_MYSLICESUBSTRUCTOPT_1_PASSWORD": "pwd",
}
os.Setenv("MYPREFIX_MYINT", "123")
defer os.Unsetenv("MYPREFIX_MYINT")
os.Setenv("MYPREFIX_MYFLOAT", "15.2")
defer os.Unsetenv("MYPREFIX_MYFLOAT")
os.Setenv("MYPREFIX_MYBOOL", "yes")
defer os.Unsetenv("MYPREFIX_MYBOOL")
os.Setenv("MYPREFIX_MYDURATION", "22s")
defer os.Unsetenv("MYPREFIX_MYDURATION")
os.Setenv("MYPREFIX_MYMAP_MYKEY", "")
defer os.Unsetenv("MYPREFIX_MYMAP_MYKEY")
os.Setenv("MYPREFIX_MYMAP_MYKEY2_MYVALUE", "asd")
defer os.Unsetenv("MYPREFIX_MYMAP_MYKEY2_MYVALUE")
os.Setenv("MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM", "456")
defer os.Unsetenv("MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM")
os.Setenv("MYPREFIX_MYSLICE", "val1,val2")
defer os.Unsetenv("MYPREFIX_MYSLICE")
os.Setenv("MYPREFIX_MYSLICEEMPTY", "")
defer os.Unsetenv("MYPREFIX_MYSLICEEMPTY")
os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_0_URL", "url1")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_0_URL")
os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_0_USERNAME", "user1")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_0_USERNAME")
os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_0_PASSWORD", "pass1")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_0_PASSWORD")
os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_1_URL", "url2")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_1_URL")
os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_1_PASSWORD", "pass2")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_1_PASSWORD")
os.Setenv("MYPREFIX_MYSLICESUBSTRUCTEMPTY", "")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCTEMPTY")
for key, val := range env {
os.Setenv(key, val)
defer os.Unsetenv(key)
}
var s testStruct
err := Load("MYPREFIX", &s)
require.NoError(t, err)
require.Equal(t, "testcontent", s.MyString)
require.Equal(t, 123, s.MyInt)
require.Equal(t, 15.2, s.MyFloat)
require.Equal(t, true, s.MyBool)
require.Equal(t, 22*myDuration(time.Second), s.MyDuration)
_, ok := s.MyMap["mykey"]
require.Equal(t, true, ok)
v, ok := s.MyMap["mykey2"]
require.Equal(t, true, ok)
require.Equal(t, "asd", v.MyValue)
require.Equal(t, 456, v.MyStruct.MyParam)
require.Equal(t, []string{"val1", "val2"}, s.MySlice)
require.Equal(t, []string{}, s.MySliceEmpty)
require.Equal(t, []mySubStruct{
{
URL: "url1",
Username: "user1",
Password: "pass1",
require.Equal(t, testStruct{
MyString: "testcontent",
MyStringOpt: stringPtr("testcontent2"),
MyInt: 123,
MyIntOpt: intPtr(456),
MyUint: 8910,
MyUintOpt: uint64Ptr(112313),
MyFloat: 15.2,
MyFloatOpt: float64Ptr(16.2),
MyBool: true,
MyBoolOpt: boolPtr(false),
MyDuration: 22000000000,
MyDurationOpt: (*myDuration)(durationPtr(30000000000)),
MyMap: map[string]*mapEntry{
"mykey": {
MyValue: "",
MyStruct: subStruct{
MyParam: 0,
},
},
"mykey2": {
MyValue: "asd",
MyStruct: subStruct{
MyParam: 456,
},
},
},
{
URL: "url2",
Password: "pass2",
MySliceString: []string{
"val1",
"val2",
},
}, s.MySliceSubStruct)
require.Equal(t, []mySubStruct{}, s.MySliceSubStructEmpty)
MySliceStringEmpty: []string{},
MySliceStringOpt: &[]string{"aa"},
MySliceSubStruct: []mySubStruct{
{
URL: "url1",
Username: "user1",
Password: "pass1",
},
{
URL: "url2",
Username: "",
Password: "pass2",
},
},
MySliceSubStructEmpty: []mySubStruct{},
}, s)
}
func FuzzLoad(f *testing.F) {
f.Add("MYPREFIX_MYINT", "a")
f.Add("MYPREFIX_MYUINT", "a")
f.Add("MYPREFIX_MYFLOAT", "a")
f.Add("MYPREFIX_MYBOOL", "a")
f.Add("MYPREFIX_MYSLICESUBSTRUCT_0_MYINT2", "a")
f.Add("MYPREFIX_MYDURATION", "a")
f.Add("MYPREFIX_MYDURATION_A", "a")
f.Fuzz(func(t *testing.T, key string, val string) {
env := map[string]string{
key: val,
}
var s testStruct
loadWithEnv(env, "MYPREFIX", &s) //nolint:errcheck
})
}