spf13 / viper

Go configuration with fangs
MIT License
27.36k stars 2.02k forks source link

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

Open wangxufire opened 7 years ago

wangxufire commented 7 years ago

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

gbolo commented 7 years ago

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 commented 7 years ago

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

Is it possible?

fopina commented 6 years ago

@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 commented 6 years ago

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

Has anyone figured this out?

fopina commented 6 years ago

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 commented 4 years ago

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 commented 3 years ago

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 commented 1 year ago

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 commented 11 months ago

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

epapbak commented 10 months ago

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 commented 10 months ago

@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 commented 10 months ago

@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.