-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unmarshal non-bound environment variables #761
Comments
Below is a wapper to unmarshal config with environment variables.
|
Hi guys, do you have a plan to support it ? I struggled with this as well and I used a solution inspired from one given by @krak3n but supporting pointers and Would be cool if mapstructure was exposing the feature of giving a tree of names based on the mapstructure tags. Then it would be easy to browse the tree and call the viper.BindEnv function with all the leaves. For the moment, here is the solution I mentioned: // bindenv: workaround to make the unmarshal work with environment variables
// Inspired from solution found here : https://github.com/spf13/viper/issues/188#issuecomment-399884438
func (b *viperConfigBuilder) bindenvs(iface interface{}, parts ...string) {
ifv := reflect.ValueOf(iface)
if ifv.Kind() == reflect.Ptr {
ifv = ifv.Elem()
}
for i := 0; i < ifv.NumField(); i++ {
v := ifv.Field(i)
t := ifv.Type().Field(i)
tv, ok := t.Tag.Lookup("mapstructure")
if !ok {
continue
}
if tv == ",squash" {
b.bindenvs(v.Interface(), parts...)
continue
}
switch v.Kind() {
case reflect.Struct:
b.bindenvs(v.Interface(), append(parts, tv)...)
default:
b.v.BindEnv(strings.Join(append(parts, tv), "."))
}
}
} |
This will be revertable when spf13/viper#761 is resolved.
Hi all, in case someone need ready-to-use-package for this feature, I've just created one based on @celian-garcia and @krak3n examples, thank you guys! |
Thanks @iamolegga, I will use it :) |
If Serializing the struct won't work correctly with |
I've also use a custom method for binding env vars without config file nor flags like so: import (
"github.com/fatih/structs"
"github.com/jeremywohl/flatten"
)
func SetupConfigFromEnv() error {
// Setup automatic env binding
viper.AutomaticEnv()
viper.SetEnvPrefix("")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Transform config struct to map
confMap := structs.Map(Config{})
// Flatten nested conf map
flat, err := flatten.Flatten(confMap, "", flatten.DotStyle)
if err != nil {
return errors.Wrap(err, "Unable to flatten config")
}
// Bind each conf fields to environment vars
for key, _ := range flat {
err := viper.BindEnv(key)
if err != nil {
return errors.Wrapf(err, "Unable to bind env var: %s", key)
}
}
return nil
} It minimises "home-made" custom code but it has to use 2 other librairies to work. |
@sagikazarmark Any news on this? Is it just a matter of a pull request? |
Any update here ? Having to use |
Here is a rough solution: type Config struct {
MyKey string `mapstructure:"mykey"`
}
// extract the keys form a struct and flatten them using a dot separator
keys := viper.ExtractKeysFromStruct(Config{}, ".")
// define keys in Viper
// this would essentially let Viper know about a key without setting a default or any value for it
// since these keys would not have a value initially, IsSet would return false, AllKeys would not return them
// AutomaticEnv detection could use these keys though
viper.Define(keys...)
// or
// BindEnvs would be the same as BindEnv for multiple keys
// though this would render AutomaticEnv less useful when used with Unmarshal
viper.BindEnvs(keys...) It would use mapstructure under the hood, so that it can stay close to the unmarshal behavior. Cases when it could fall short: type Config struct {
MyKey string `mapstructure:"mykey"`
// The default value here is nil, so fields of the config would probably not show up by default?
SubConfig *SubConfig
}
type SubConfig struct {
MyKey string `mapstructure:"mykey"`
} WDYT? |
If you use mapstructure as it is now to implement Personally I believe it's best if |
That's entirely correct, we would have to workaround that issue somehow. See the issue above I opened in the mapstructure repo.
I think it would be very hard to do, if possible at all. The way it works currently, is Unmarshal gets a list of values from the Viper instance and tries to unmarshal them onto the struct. At this point environment variables are already evaluated. I don't think that side effect is actually a problem, because it's exactly the same thing when you set a default for example: you define a key in one of the config sources and that key in this case would be exactly the same for all sources (if you were to call What |
👀 |
In cases where there isn't a configuration file, none of the fields on the struct are bound to environment variables unless BindEnv was explicitly called. AutomaticEnv essentially doesn't work with the config file per spf13#761. If you need to support this scenario, call ReadConfigFrom on the destination struct before calling Unmarshal. Signed-off-by: Carolyn Van Slyck <[email protected]>
In cases where there isn't a configuration file, none of the fields on the struct are bound to environment variables unless BindEnv was explicitly called. AutomaticEnv essentially doesn't work with the config file per spf13#761. If you need to support this scenario, call ReadConfigFrom on the destination struct before calling Unmarshal. Signed-off-by: Carolyn Van Slyck <[email protected]>
In cases where there isn't a configuration file, none of the fields on the struct are bound to environment variables unless BindEnv was explicitly called. AutomaticEnv essentially doesn't work with the config file per spf13#761. If you need to support this scenario, call ReadConfigFrom on the destination struct before calling Unmarshal. Signed-off-by: Carolyn Van Slyck <[email protected]>
In cases where there isn't a configuration file, none of the fields on the struct are bound to environment variables unless BindEnv was explicitly called. AutomaticEnv essentially doesn't work with the config file per spf13#761. If you need to support this scenario, call SetDefaultsFrom on the destination struct before calling Unmarshal. Signed-off-by: Carolyn Van Slyck <[email protected]>
There is a way to automate BindEnv envoking with retrieving mapstructure keys from Config struct: package viper
import (
"os"
"testing"
"github.com/mitchellh/mapstructure"
)
func TestUnmarshal_AutomaticEnv(t *testing.T) {
os.Setenv("MYKEY", "myvalue")
type Config struct {
MyKey string `mapstructure:"mykey"`
}
var c Config
AutomaticEnv()
// ADD START
envKeysMap := &map[string]interface{}{}
if err := mapstructure.Decode(c, &envKeysMap); err != nil {
t.Fatal(err)
}
for k := range *envKeysMap {
if bindErr := viper.BindEnv(k); bindErr != nil {
t.Fatal(err)
}
}
// ADD END
if err = Unmarshal(&c); err != nil {
t.Fatal(err)
}
if c.MyKey != "myvalue" {
t.Error("failed to unmarshal automatically loaded environment variable")
}
os.Clearenv()
} |
I have just tested with Env and PFlags, I have to use |
…ults are defined within yaml files For more information see: spf13/viper#761
Added default value to `DDNS_TOKEN` so that Viper would be able to find the key and matching env. More details can be found in spf13/viper#761. Closes #31
Added default value to `DDNS_TOKEN` so that Viper would be able to find the key and matching env. More details can be found in spf13/viper#761. Closes skibish#31
For us this worked... `viper.AutomaticEnv()
|
Hey friends, i think i found a solution generic enough
|
Another much simpler solution, that works at least for simple structs would be the other way around: Binding all envs that have the prefix. Maybe it helps someone, so I will leave it here: viper.SetEnvPrefix(EnvPrefix)
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
for _, e := range os.Environ() {
split := strings.Split(e, "=")
k := split[0]
if strings.HasPrefix(k, EnvPrefix) {
viper.BindEnv(strings.Join(strings.Split(k, "_")[1:], "."))
}
} |
I've been wanting to turn this into a PR of some kind but haven't been able to. Its a function that maps env variables to a struct using viper. You can define the env name and default in the struct tag. const tagName = "envname"
var (
ErrKindInvalid = errors.New("Kind is invalid")
ErrPointerNeeded = errors.New("Expecting a pointer to an object")
IntType = reflect.TypeOf(int(0))
StringType = reflect.TypeOf("")
Uint16Type = reflect.TypeOf(uint16(0))
Int64Type = reflect.TypeOf(int64(0))
Float64Type = reflect.TypeOf(float64(0))
BoolType = reflect.TypeOf(false)
DurationType = reflect.TypeOf(time.Duration(0))
StringSliceType = reflect.TypeOf([]string{})
ByteType = reflect.TypeOf(byte(0))
)
// BindToViper takes a pointer to an object and binds the environment named
// variables to viper and the passed in struct object. Default values can be
// passed in as a second value (comma separated)
// Ex
// type ViperStruct struct {
// Boolean bool `envname:"BOOLEAN,true"`
// }
//
// Supported types include
// - string
// - []string
// - int
// - uint16
// - bool
// - int64 / time.Duration (same type)
// - byte
// - float64
// TODO: recursive nested object handling
func BindToViper(obj interface{}, prefix string) error {
var valuePtr reflect.Value
var value reflect.Value
objType := reflect.TypeOf(obj)
if objType.Kind() == reflect.Ptr {
valuePtr = reflect.ValueOf(obj)
value = reflect.Indirect(valuePtr)
} else {
return ErrPointerNeeded
}
for i := 0; i < value.NumField(); i++ {
// Get the field tag value
structType := value.Type()
field := structType.Field(i)
// fieldType := field.Type
tag := field.Tag.Get(tagName)
// Skip if tag is not defined or ignored
if tag == "" || tag == "-" {
continue
}
tagArgs := strings.Split(tag, ",")
envName := buildName(prefix, tagArgs[0])
fieldVal := valuePtr.Elem().Field(i)
err := setViperDefaults(tagArgs, envName, field.Type)
if err != nil {
return err
}
err = setViperValue(tagArgs, envName, field.Type, fieldVal)
if err != nil {
return err
}
}
return nil
}
func isNil(i interface{}) bool {
if i == nil {
return true
}
switch reflect.TypeOf(i).Kind() {
case reflect.Ptr, reflect.Map, reflect.Interface, reflect.Array, reflect.Chan, reflect.Slice, reflect.Func:
return reflect.ValueOf(i).IsNil()
}
return false
}
// setViperValue will assign the struct field to the value as contained in
// viper. It's the equivalent of calling
// strct.Field = viper.Get("field")
// but relatively generic
func setViperValue(tagArgs []string, envName string, t reflect.Type, v reflect.Value) error {
switch t {
case StringType:
v.SetString(viper.GetString(envName))
case StringSliceType:
sVal := reflect.ValueOf(viper.GetStringSlice(envName))
v.Set(sVal)
case IntType:
sVal := reflect.ValueOf(viper.GetInt(envName))
v.Set(sVal)
case Uint16Type:
sVal := reflect.ValueOf(uint16(viper.GetInt(envName)))
v.Set(sVal)
case BoolType:
sVal := reflect.ValueOf(viper.GetBool(envName))
v.Set(sVal)
case Int64Type: // Int64 works for both int64 and derivative types
sVal := reflect.ValueOf(viper.GetInt64(envName))
v.Set(sVal)
case DurationType:
sVal := reflect.ValueOf(viper.GetDuration(envName))
v.Set(sVal)
case ByteType:
sVal := reflect.ValueOf(byte(viper.GetInt(envName)))
v.Set(sVal)
case Float64Type:
sVal := reflect.ValueOf(viper.GetFloat64(envName))
v.Set(sVal)
}
return nil
}
// setViperDefaults will parse the struct tags and pull out the defaults. It'll
// try to parse the default into the `kind` of the struct field and set that as
// a viper default value
func setViperDefaults(tagArgs []string, envName string, t reflect.Type) error {
// Don't overwrite any defaults previously set. We don't want the struct's
// defaults to overwrite any that were set before it because we want to make
// sure the struct defaults don't take precedence
if viper.IsSet(envName) || len(tagArgs) < 2 {
return nil
}
switch t {
case IntType:
val, err := strconv.Atoi(tagArgs[1])
if err != nil {
return err
}
viper.SetDefault(envName, val)
case DurationType:
durVal, err := time.ParseDuration(tagArgs[1])
if err != nil {
// return the original err
return err
}
viper.SetDefault(envName, durVal)
case Int64Type:
val, err := strconv.ParseInt(tagArgs[1], 10, 64)
if err != nil {
return err
}
viper.SetDefault(envName, val)
case StringType:
viper.SetDefault(envName, tagArgs[1])
case Uint16Type:
val, err := strconv.ParseUint(tagArgs[1], 10, 16)
if err != nil {
return err
}
viper.SetDefault(envName, val)
case BoolType:
val, err := strconv.ParseBool(tagArgs[1])
if err != nil {
return err
}
viper.SetDefault(envName, val)
case StringSliceType:
// viper only supports string slices, so no further processing is needed
viper.SetDefault(envName, tagArgs[1])
case ByteType:
viper.SetDefault(envName, tagArgs[1])
case Float64Type:
val, err := strconv.ParseFloat(tagArgs[1], 64)
if err != nil {
return err
}
viper.SetDefault(envName, val)
}
return nil
}
// buildName is part of the Prefix feature. It will prepend the prefix to the
// name or, if no prefix is set, will return the name directly
func buildName(prefix, name string) string {
if prefix != "" {
return strings.Join([]string{strings.ToUpper(prefix), strings.ToUpper(name)}, "_")
} else {
return name
}
} Usage example: type Config struct {
JWTKey string `envname:"JWT"`
MaxAge time.Duration `envname:"MAX_AGE,5m"`
}
func main(){
viper.SetConfigType("yaml")
viper.SetConfigName("config")
viper.AddConfigPath(".")
viper.ReadInConfig()
viper.AutomaticEnv()
cfg := &Config{}
BindToViper(&cfg, "")
} |
I know this is closed, but I was surprised to learn this happens and only learned it through some searching (that managed to hit https://renehernandez.io/snippets/bind-environment-variables-to-config-struct-with-viper/). It is really surprising that |
This is going to be an umbrella issue for other open issues for this problem.
These are:
viper.Unmarshal
to take environment variables into account? #212Problem
Currently Viper cannot unmarshal automatically loaded environment variables.
Here is a failing test case reproducing the issue:
Root cause
Automatic environment variable loading works as follows:
viper.GetString("mykey")
Unmarshal, however, doesn't work by requesting a key, quite the opposite: it tries to unmarshal existing keys onto the given structure. Since automatically loaded environment variables are not in the list of existing keys (unlike defaults and bind env vars), these variables won't be unmarshaled.
Workaround
BindEnv
to manually bind environment variablesSetDefault
to set a default value (even an empty one), so Viper can find the key and try to find a matching env varPossible solutions
The linked issues list a number of workarounds to fix this issue.
Personally, I'm in favor of the following:
map[string]interface{}
. That will give use a nested structure of all the keys otherwise mapstructure expects.Although I think that this is currently the best available solution, it doesn't mean we can't do better, so please do suggest if you have an idea.
The text was updated successfully, but these errors were encountered: