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

How to get a array config value from env? #339

Open
wangxufire opened this issue Apr 24, 2017 · 12 comments
Open

How to get a array config value from env? #339

wangxufire opened this issue Apr 24, 2017 · 12 comments

Comments

@wangxufire
Copy link

e.g I want get the hobbies(in README) value from env.

@gbolo
Copy link

gbolo commented Jun 20, 2017

This is a good question. So we can retrieve this list like this: viper.GetStringSlice("hobbies").

Example Code:

package main

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

var yamlExample = []byte(`
name: steve
hobbies:
- skateboarding
- snowboarding
`)

func main(){
	viper.SetConfigType("yaml")
	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	viper.AutomaticEnv()
	viper.ReadConfig(bytes.NewBuffer(yamlExample))

	t := viper.GetStringSlice("hobbies")
	fmt.Printf( "Type: %T,  Size: %d \n", t, len(t) )
	for i, v := range t {
		fmt.Printf("Index: %d, Value: %v\n", i, v )
	}
}

When I run this without setting an environment variable override I get:

$ go run main.go 
Type: []string,  Size: 2 
Index: 0, Value: skateboarding
Index: 1, Value: snowboarding

Overriding this through env:

$ HOBBIES="devops go docker ansible" go run main.go 
Type: []string,  Size: 4 
Index: 0, Value: devops
Index: 1, Value: go
Index: 2, Value: docker
Index: 3, Value: ansible

@spf13 perhaps we should include this in the main README.md? Also what if we have a list of maps, how can we override that?

@kyroy
Copy link

kyroy commented Nov 10, 2017

Also what if we have a list of maps, how can we override that?

Is it possible?

@fopina
Copy link

fopina commented Sep 11, 2018

@spf13 perhaps we should include this in the main README.md? Also what if we have a list of maps, how can we override that?

Definitely, just spent quite some time going through the code to find how to pass stringslices through ENV...
Also some way to support spaces in the slices would be nice

@kchristidis
Copy link

Also what if we have a list of maps, how can we override that?

Has anyone figured this out?

@fopina
Copy link

fopina commented Nov 13, 2018

Also what if we have a list of maps, how can we override that?

Has anyone figured this out?

going back to code to find that as I needed as well (but haven't tested yet) https://github.com/spf13/cast/blob/master/caste.go#L876

So if we're using GetStringMap it ends there where, if value is string, it will be parsed as JSON... 👍 for doc updates again...

@manya0393
Copy link

Also what if we have a list of maps, how can we override that?

Has anyone figured this out?

going back to code to find that as I needed as well (but haven't tested yet) https://github.com/spf13/cast/blob/master/caste.go#L876

So if we're using GetStringMap it ends there where, if value is string, it will be parsed as JSON... 👍 for doc updates again...
Hey we tried to use 'GetStringMap' an dalso 'GetStringMapStringSlice' for the list of maps variables but not able to get any luck. Please help us by giving some example that how can we fetch the value of list of map variable.

For Example- variable will look like-
backend-pool-ip-addresses = [{"IpAddress" = "10.0.0.5"}, {"IpAddress" = "10.0.0.6"}]
which is reading like- backend-pool-ip-addresses:[map[IpAddress:10.0.0.5] map[IpAddress:10.0.0.6]]

If anyone can help us as this is imp and urgent project delivery is pending.

@vasily-kirichenko
Copy link

The solution does not seem to work with GetIntSlice:

package main

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

var yamlExample = []byte(`
name: steve
hobbies:
- 10
- 20
`)

func main() {
	viper.SetConfigType("yaml")
	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	viper.AutomaticEnv()
	viper.ReadConfig(bytes.NewBuffer(yamlExample))

	t := viper.GetIntSlice("hobbies") // <----------------------
	fmt.Printf("Type: %T,  Size: %d \n", t, len(t))
	for i, v := range t {
		fmt.Printf("Index: %d, Value: %v\n", i, v)
	}
}
go run .
Type: []int,  Size: 2 
Index: 0, Value: 10
Index: 1, Value: 20
HOBBIES="100 200" go run .
Type: []int,  Size: 0 

@BERZERKCOOLeST
Copy link

I met the situation that need to parse the env variables which type is a list of map to a structure.

So after a try, i wrote codes below to try to solve.

I parodied the function, mapstructure.StringToSliceHookFunc.

Maybe this is a not good way, but it may be for reference.

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"reflect"
	"strings"

	"github.com/mitchellh/mapstructure"
	"github.com/spf13/viper"
)

var yamlExample = []byte(`
name: steve
hobbies:
- id: a
  name: aaa
- id: b
  name: bbb
`)

var envExample = `[
    {"id":"a","name":"no_aaa"}
  ]`

type Conf struct {
	Name    string `json:"name"`
	Hobbies []struct {
		Id   string `json:"id"`
		Name string `json:"name"`
	} `json:"hobbies"`
}

func main() {
	os.Setenv("NAME", "who")
	os.Setenv("HOBBIES", envExample)

	viper.SetConfigType("yaml")
	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	viper.AutomaticEnv()
	viper.ReadConfig(bytes.NewBuffer(yamlExample))

	var conf *Conf
	err := viper.Unmarshal(&conf, func(dc *mapstructure.DecoderConfig) {
		dc.DecodeHook = mapstructure.ComposeDecodeHookFunc(
			StringToStructHookFunc(),
			StringToSliceWithBracketHookFunc(),
			dc.DecodeHook)
	})
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(fmt.Sprintf("%+v", conf)) // &{Name:who Hobbies:[{Id:a Name:no_aaa}]}

}

func StringToSliceWithBracketHookFunc() mapstructure.DecodeHookFunc {
	return func(
		f reflect.Kind,
		t reflect.Kind,
		data interface{}) (interface{}, error) {
		if f != reflect.String || t != reflect.Slice {
			return data, nil
		}

		raw := data.(string)
		if raw == "" {
			return []string{}, nil
		}
		var slice []json.RawMessage
		err := json.Unmarshal([]byte(raw), &slice)
		if err != nil {
			return data, nil
		}

		var strSlice []string
		for _, v := range slice {
			strSlice = append(strSlice, string(v))
		}
		return strSlice, nil
	}
}

func StringToStructHookFunc() mapstructure.DecodeHookFunc {
	return func(
		f reflect.Type,
		t reflect.Type,
		data interface{},
	) (interface{}, error) {
		if f.Kind() != reflect.String ||
			(t.Kind() != reflect.Struct && !(t.Kind() == reflect.Pointer && t.Elem().Kind() == reflect.Struct)) {
			return data, nil
		}
		raw := data.(string)
		var val reflect.Value
		// Struct or the pointer to a struct
		if t.Kind() == reflect.Struct {
			val = reflect.New(t)
		} else {
			val = reflect.New(t.Elem())
		}

		if raw == "" {
			return val, nil
		}
		err := json.Unmarshal([]byte(raw), val.Interface())
		if err != nil {
			return data, nil
		}
		return val.Interface(), nil
	}
}

@comminutus
Copy link

Has anyone figured out how to pass a list of maps through an environment variable?

@epapbak
Copy link

epapbak commented Jan 10, 2024

My solution, if it helps:

func handleBrokersConfigEnvVar() {
	// if BROKERS if found in env, handle it
	const key = "brokers"

        // Get the value as string
	brokers := viper.GetString(key)

        // unmarshall it into a []map[string]interface{}, which is what Viper knows how to work with
	resultArr := make([]map[string]interface{}, len(brokers))
	err := json.Unmarshal([]byte(brokers), &resultArr)
	if err != nil {
		log.Error().Msgf("Error decoding content of CCX_NOTIFICATION_WRITER__BROKERS env var: %v\n", err)
	}


        // Here's thhe trick: give the []map[string]interface{} to Viper
	viper.Set(key, resultArr)
}

Then in my code that actually loads the configuration:


	// override configuration from env if there's variable in env
	const envPrefix = "THE_ENV_PREFIX"

	viper.AutomaticEnv()
	viper.SetEnvPrefix(envPrefix)
	viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "__"))

        // call the helper function before unmarshalling to your struct
	handleBrokersConfigEnvVar()

	err = viper.Unmarshal(&configuration)
	if err != nil {
		return configuration, err
	}

edit: as we've got lots of unit tests, in the end my helper function looks like this, with some additional checks:

func handleBrokersConfigEnvVar() {
	// if BROKERS if found in env, handle it
	const key = "brokers"
	brokers := viper.GetString(key)

	if brokers == "" {
		log.Warn().Msg("nothing to do for the brokers env var")
		// When you remove an environment variable, Viper might still have the previously
		// set value cached in memory and doesn't automatically revert to the value from
		// the configuration file. So this line is purely here so our unit tests pass
		viper.Set(key, nil)
		return
	}

	resultArr := make([]map[string]interface{}, len(brokers))
	err := json.Unmarshal([]byte(brokers), &resultArr)
	if err != nil {
		log.Error().Msgf("Error decoding content of BROKERS env var: %v", err)
	}

	viper.Set(key, resultArr)
}

@epapbak
Copy link

epapbak commented Jan 10, 2024

@comminutus with the solution I proposed, you simply pass a JSON array of objects as environment variable. In my case, it's Kafka brokers' configuration, so I add this env var: BROKERS='[{"address":"address1:port1","topic":"topic1"},{"address":"address2:port2","topic":"topic2"}]'

@comminutus
Copy link

@epapbak unfortunately, I'm dependent upon a project which uses viper and can't modify the source. It'd be better to have first-order support for this in viper itself.

mnvr added a commit to ente-io/ente that referenced this issue Nov 28, 2024
…gular admin (#4228)

## Description

This allows working around an issue with Viper where it doesn't properly
unmarshal GetIntSlice from environment variables. Seems like this is a
known issue and is quite old. I'm assuming there isn't plans to fix it
on the Viper side.

spf13/viper#339

## Tests

I'll be brutally honest, I don't know how to run Go, but this seemed
like a pretty quick low hanging fix.
joschi added a commit to joschi/gotosocial that referenced this issue Dec 7, 2024
Maps are currently not supported in environment variables.

See also spf13/viper#339
joschi added a commit to joschi/gotosocial that referenced this issue Dec 8, 2024
Maps are currently not supported in environment variables.

See also spf13/viper#339
joschi added a commit to joschi/gotosocial that referenced this issue Dec 10, 2024
Maps are currently not supported in environment variables.

See also spf13/viper#339
joschi added a commit to joschi/gotosocial that referenced this issue Dec 14, 2024
Maps are currently not supported in environment variables.

See also spf13/viper#339
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants