Skip to content
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

Does viper require a config file in order to use ENV variables? #584

Closed
jsirianni opened this issue Oct 23, 2018 · 14 comments · Fixed by #1429
Closed

Does viper require a config file in order to use ENV variables? #584

jsirianni opened this issue Oct 23, 2018 · 14 comments · Fixed by #1429

Comments

@jsirianni
Copy link

Hi, I have an application that can use both a config file and env variable.

My initConfig() method looks like this:

if err := viper.ReadInConfig(); err == nil {
	fmt.Println("Using config file:", viper.ConfigFileUsed())
} else {
	fmt.Println("Config file not found:", cfgFile)
}

viper.SetEnvPrefix("GC")
viper.AutomaticEnv()

err := viper.Unmarshal(&config)
if err != nil {
	fmt.Println(err.Error())
	os.Exit(1)
}

unmarshal drops the config into a struct, and it works well.

I am facing an issue where if my config file does not exist (or is empty), viper seems to completely ignore any environment variables. I have verified that viper can see the env variables, by printing them to standard out. My workaround is to deploy the config file, with all the correct keys but no values. Viper then correctly overrides the config file with my environment variables.

Am I wrong to assume that Viper should be able to use environment variables when no config file is found?

@matejkramny
Copy link

+1 just spent a lot of time finding why env variables don't bind to the config file. Simply adding the key to the yaml file made it work

@jsirianni
Copy link
Author

Looking at viper.go, I find this comment:

// Viper is a prioritized configuration registry. It
// maintains a set of configuration sources, fetches
// values to populate those, and provides them according
// to the source's priority.
// The priority of the sources is the following:
// 1. overrides
// 2. flags
// 3. env. variables
// 4. config file
// 5. key/value store
// 6. defaults
//
// For example, if values from the following sources were loaded:
//
//  Defaults : {
//  	"secret": "",
//  	"user": "default",
//  	"endpoint": "https://localhost"
//  }
//  Config : {
//  	"user": "root"
//  	"secret": "defaultsecret"
//  }
//  Env : {
//  	"secret": "somesecretkey"
//  }
//
// The resulting config will have the following values:
//
//	{
//		"secret": "somesecretkey",
//		"user": "root",
//		"endpoint": "https://localhost"
//	}

The example shows secret being in all three configuration sources, and being overridden accordingly. What they do not seem to mention is that config is required. I find it hard to believe that it should be this way, considering values found in a config work just fine even if you have not specified defaults anywhere.

It would be nice to know if this is desired behavior, so I can build my applications accordingly. My main driver for avoiding a configuration file is that I do not wish to have configurations baked into my docker containers. I can deploy empty configurations, however, that is an additional (and unnecessary) stop in my build process.

@renannprado
Copy link

@jsirianni totally agree with you. I was surprised actually that I'm forced to configure AddConfigPath, SetConfigName, SetConfigType, AutomaticEnv and set a default value like this (viper.SetDefault("TEST", "")) or via file.
IMO the Unmarshal should bind out of the box to the environment variables, i.e. running AutomaticEnv should be enough for Unmarshal to work properly. I'm not sure if easy or feasible (given the maintainers want to keep backwards compatibility probably), but I'd say this sounds like the logical behavior to me.

@sagikazarmark
Copy link
Collaborator

sagikazarmark commented Jan 4, 2019

The problem is actually Unmarshal in this case. If you don't set a config and call viper.Get* you will receive the desired value. The AutomaticEnv feature (when turned on) will check the environment variable even if it isn't registered.

Unmarshal works a bit differently: it collects the registered keys and passes them with their values to the mapstructure package which binds the values to the appropriate keys of the structure. In this case there is no "request" nothing to compare against.

I agree that this is not optimal, but given Unmarshal was added later I can hardly categorize this as a regression.

Unfortunately it's not easy to fix as environment variables are transformed in various ways:

  • a prefix is added
  • env key replacer is applied

Even if the prefix can be transformed back, the env key replacer cannot be "reversed".

It would probably worth checking to see if somehow we can get the keys from mapstructure or hook into the process there somehow. (Update: unfortunately keys are not available in the decode hooks)

(An alternative could be doing a "reverse decode": grab an empty struct and "decode" it into a map[string]interface{}. Recursively go through the map to collect the keys, flatten them and bind those keys. It's not a nice solution, but at least you can somewhat automate it.)

In the meantime, using BindEnv manually is the only way unfortunately (or manual unmarshaling).

Normally I define default configuration wherever possible, so it's really just those few keys I have to manually bind (database host, port, user, password, name; redis host and password).

drakkan added a commit to drakkan/sftpgo that referenced this issue Sep 4, 2019
viper will not use env vars if no configuration file is found

spf13/viper#584

As workaround we could manually bind/set a default for each configuration
option using viper.SetDefault("key") and then generate a default config
using viper.Get("key").
This manual solution is error prone and it will become increasingly
difficult to maintain since the configuration options will grow, so
we avoid it for now.

Let's see if viper will solve this issue

Fixes #35
@sagikazarmark
Copy link
Collaborator

Please see and follow the issue above. ☝️

@niondir
Copy link

niondir commented Sep 11, 2020

I'm modeling my whole config as struct. To make all environment variables work, even when the config file does not contain the key I first load my defaults from a struct like this:

func Init() {
	initDefaultValues()
	/* ... */
	viper.AutomaticEnv()
	/* ... */
	viper.ReadInConfig()
}


func initDefaultValues() {
	defaultConfig = &Config{}
	defaultConfig.Server.Port = 1272

	loadConfigFromStruct(defaultConfig)
}

func loadConfigFromStruct(cfg interface{}) {
	cfgMap := make(map[string]interface{})
	err := mapstructure.Decode(cfg, &cfgMap)
	if err != nil {
		logrus.WithError(err).Fatalf("failed to marshal default config")
		os.Exit(1)
	}

	cfgJsonBytes, err := json.Marshal(&cfgMap)
	if err != nil {
		logrus.WithError(err).Fatalf("failed to marshal default config")
		os.Exit(1)
	}

	viper.SetConfigType("json")
	err = viper.ReadConfig(bytes.NewReader(cfgJsonBytes))
	if err != nil {
		logrus.WithError(err).Fatalf("failed to load default config")
		os.Exit(1)
	}
}

farrago added a commit to farrago/featmap that referenced this issue Mar 27, 2021
This adds the ability to override config settings with an environment
variable in the form "FEATMAP_<uppercase setting name>".
e.g. `FEATMAP_PORT=8080` would override the `port` value in `conf.json`.

Due to the way Viper works, this cannot load from *only* env vars,
so the config file is still needed and the env vars can override it.
See spf13/viper#584

To help protect users from this issue, this change also adds
verification that essential config settings have been configured.

# Tests

- Test that configuration is still loaded from the config file
- Test that environment variables override the values in the config file
- Test that missing settings correctly report an error
farrago added a commit to farrago/featmap that referenced this issue Apr 17, 2021
This adds the ability to override config settings with an environment
variable in the form "FEATMAP_<uppercase setting name>".
e.g. `FEATMAP_PORT=8080` would override the `port` value in `conf.json`.

Due to the way Viper works, this cannot load from *only* env vars,
so the config file is still needed and the env vars can override it.
See spf13/viper#584

To help protect users from this issue, this change also adds
verification that essential config settings have been configured.

# Tests

- Test that configuration is still loaded from the config file
- Test that environment variables override the values in the config file
- Test that missing settings correctly report an error
@iurydias
Copy link

Facing the same issue... and it seems to have a not easy solution :(

@drakkan
Copy link

drakkan commented Jul 27, 2021

Facing the same issue... and it seems to have a not easy solution :(

you can simply use viper.SetDefault. This way you can override from env even without a config file

@iurydias
Copy link

Facing the same issue... and it seems to have a not easy solution :(

you can simply use viper.SetDefault. This way you can override from env even without a config file

it works... thank you so much!

@nabeelsaabna
Copy link

nabeelsaabna commented Jan 15, 2022

I hope it's not a long answer to this problem (not tested with maps)

Below code will:

  • go over the Settings struct that contains the configuration hierarchy of my project
  • adds recursively all the fields under it to viper with default of empty value

which leads to all of these fields to get picked up from the environment

package main

import (
	"fmt"
	"github.com/spf13/viper"
	"reflect"
	"strings"
)

type (
	Settings struct {
		Vitals   Vitals
		Excludes []string `yaml:"excludes,omitempty"`
	}

	Vitals struct {
		Product PIdentifier
		Project PIdentifier
	}

	PIdentifier struct {
		Name  string
		Token string
	}
)

func addKeysToViper(v *viper.Viper) {
	var reply interface{} = Settings{}
	t := reflect.TypeOf(reply)
	keys := getAllKeys(t)
	for _,key := range keys {
		fmt.Println(key)
		v.SetDefault(key, "")
	}
}

func getAllKeys(t reflect.Type) []string {
	var result []string

	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		n := strings.ToUpper(f.Name)
		if reflect.Struct == f.Type.Kind() {
			subKeys := getAllKeys(f.Type)
			for _, k := range subKeys {
				result = append(result, n+"."+k)
			}
		} else {
			result = append(result, n)
		}
	}

	return result
}

func main() {
	v := viper.New()
	addKeysToViper(v)
	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	v.SetEnvPrefix("ABC")
	v.AutomaticEnv()

	fmt.Println(v.WriteConfigAs("out.yaml"))
}

the printed output from the addKeysToViper :

VITALS.PRODUCT.NAME
VITALS.PRODUCT.TOKEN
VITALS.PROJECT.NAME
VITALS.PROJECT.TOKEN
EXCLUDES

env vars used in this test:

ABC_VITALS_PRODUCT_NAME=a
ABC_VITALS_PRODUCT_TOKEN=b
ABC_EXCLUDES=c

the resulting value for out.yaml :

excludes: c
vitals:
  product:
    name: a
    token: b
  project:
    name: ""
    token: ""

@qwerty22121998
Copy link

my workaround, seem silly but it worked

func Parse(i interface{}) error {
	r := reflect.TypeOf(i)
	for r.Kind() == reflect.Ptr {
		r = r.Elem()
	}
	for i := 0; i < r.NumField(); i++ {
		env := r.Field(i).Tag.Get("mapstructure")
		if err := viper.BindEnv(env); err != nil {
			return err
		}
	}
	return viper.Unmarshal(i)
}

@mccallry
Copy link

mccallry commented Aug 10, 2022

The above example from @qwerty22121998 is a solid work around for flat structures; however, it struggles in that it doesn't deal with nested structures (Go Playground Link).

package main

import (
	"fmt"
	"os"
	"reflect"
	"strings"

	"github.com/spf13/viper"
)

type Config struct {
	Foo string `mapstructure:"FOO"`
	Bar string `mapstructure:"BAR"`
	Baz FooBar `mapstructure:"BAZ"`
}

type FooBar struct {
	Foo string `mapstructure:"FOO"`
	Bar string `mapstructure:"BAR"`
}

func scaffold() {
	os.Setenv("FOO", "1")
	os.Setenv("BAR", "A")
	os.Setenv("BAZ.FOO", "2")
	os.Setenv("BAZ.BAR", "B")
}

func parseEnv(i interface{}) error {
	r := reflect.TypeOf(i)
	for r.Kind() == reflect.Ptr {
		r = r.Elem()
	}
	for i := 0; i < r.NumField(); i++ {
		env := r.Field(i).Tag.Get("mapstructure")
		if err := viper.BindEnv(env); err != nil {
			return err
		}
	}
	return viper.Unmarshal(i)
}

func main() {
	scaffold()

	var c Config
	if e := parseEnv(&c); e != nil {
		panic(e)
	}
	fmt.Println(fmt.Sprintf("%+v", c))
}

The above will output:

{Foo:1 Bar:A Baz:{Foo: Bar:}}

Program exited.

I would suggest making a few changes. In particular, we want to check if the current property is a struct and recursively try and build the environment variables. If you want to use this strategy, I would propose changes like the following (Go Playground Link):

package main

import (
	"fmt"
	"os"
	"reflect"

	"github.com/spf13/viper"
)

type Config struct {
	Foo string `mapstructure:"FOO"`
	Bar string `mapstructure:"BAR"`
	Baz FooBar `mapstructure:"BAZ"`
}

type FooBar struct {
	Foo string    `mapstructure:"FOO"`
	Bar string    `mapstructure:"BAR"`
	Baz FooBarBaz `mapstructure:"BAZ"`
}

type FooBarBaz struct {
	Foo string `mapstructure:"FOO"`
	Bar string `mapstructure:"BAR"`
}

func scaffold() {
	os.Setenv("FOO", "1")
	os.Setenv("BAR", "A")
	os.Setenv("BAZ.FOO", "2")
	os.Setenv("BAZ.BAR", "B")
	os.Setenv("BAZ.BAZ.FOO", "3")
	os.Setenv("BAZ.BAZ.BAR", "C")
}

func parseEnv(i interface{}, parent, delim string) error {
	// Retrieve the underlying type of variable `i`.
	r := reflect.TypeOf(i)

	// If `i` is of type pointer, retrieve the referenced type.
	if r.Kind() == reflect.Ptr {
		r = r.Elem()
	}

	// Iterate over each field for the type. By default, there is a single field.
	for i := 0; i < r.NumField(); i++ {
		// Retrieve the current field and get the `mapstructure` tag.
		f := r.Field(i)
		env := f.Tag.Get("mapstructure")

		// By default, set the key to the current tag value. If a parent value was passed in
		//	prepend the parent and the delimiter.
		if parent != "" {
			env = parent + delim + env
		}

		// If it's a struct, only bind properties.
		if f.Type.Kind() == reflect.Struct {
			t := reflect.New(f.Type).Elem().Interface()
			parseEnv(t, env, delim)
			continue
		}

		// Bind the environment variable.
		if e := viper.BindEnv(env); e != nil {
			return e
		}
	}
	return viper.Unmarshal(i)
}

func main() {
	scaffold()

	var c Config
	if e := parseEnv(&c, "", "."); e != nil {
		panic(e)
	}
	fmt.Println(fmt.Sprintf("%+v", c))
}

The above will output:

{Foo:1 Bar:A Baz:{Foo:2 Bar:B Baz:{Foo:3 Bar:C}}}

Program exited.

For anyone else who ends up searching up this issue and coming here, this is because Viper, at its core, is primarily meant to handle file-based environment configurations. In fact, the Unmarshal method, when you trace its internal calls, doesn't even interact with the automaticEnvApplied flag in the Viper struct. This is because the Get method call which would be able to retrieve environment variables never gets to execute. AllKeys does not show as there being any environment variables because none of the internal maps are populated at that time. Essentially, unless the variables are being shadowed, the environment variables are not typically going to be seen unless explicitly bound.

If you want to use environment variables by themselves you need to either:

  1. Create a solution which adds the environment variables into the internal maps, such as leveraging the BindEnv method as above.
  2. Retrieve the environment values using the Get method, which does interact with the AutomaticEnv flag.

I would probably recommend sticking to one of the above solutions which binds variables into one of the internal maps used by Viper instead of using some recursive method in which you uses the reflection package and viper.Get to build the struct. At that point, you may as well just not use Viper at all and grab the variables directly from os.Getenv.

@fclairamb
Copy link

fclairamb commented Sep 30, 2022

I frequently come back to this issue because we still have codes that use viper.

For those who are tired of these issues (which essentially come from bad design), you should have a look at koanf.
The number of ⭐ isn't always related with quality, in terms of design/code or even support.

@fauzanfebrian
Copy link

my workaround, seem silly but it worked

func Parse(i interface{}) error {
	r := reflect.TypeOf(i)
	for r.Kind() == reflect.Ptr {
		r = r.Elem()
	}
	for i := 0; i < r.NumField(); i++ {
		env := r.Field(i).Tag.Get("mapstructure")
		if err := viper.BindEnv(env); err != nil {
			return err
		}
	}
	return viper.Unmarshal(i)
}

Thanks, this helped me alot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet