如何将数组中的配置项绑定到环境变量中?

huangapple go评论78阅读模式
英文:

How to bind config items in an array to environment variables

问题

以下是我的配置文件,采用toml格式。

[[hosts]]
name = "host1"
username = "user1"
password = "password1"

[[hosts]]
name = "host2"
username = "user2"
password = "password2"

这是加载配置文件的代码:

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

type Config struct {
    Hosts []Host
}

type Host struct {
    Name     string `mapstructure:"name"`
    Username string `mapstructure:"username"`
    Password string `mapstructure:"password"`
}

func main() {
    viper.AddConfigPath(".")
    viper.AddConfigPath("./config")
    viper.SetConfigName("app")

    if err := viper.ReadInConfig(); err != nil {
        return nil, fmt.Errorf("error reading config file, %v", err)
    }

    config := new(Config)
    if err := viper.Unmarshal(config); err != nil {
        return nil, fmt.Errorf("error parsing config file, %v", err)
    }

    var username, password string

    for i, h := range config.Hosts {
        if len(h.Name) == 0 {
            return nil, fmt.Errorf("name not defined for host %d", i)
        }

        if username = os.Getenv(strings.ToUpper(h.Name) + "_" + "USERNAME"); len(username) > 0 {
            config.Hosts[i].Username = username
        } else if len(h.Username) == 0 {
            return nil, fmt.Errorf("username not defined for %s", e.Name)
        }

        if password = os.Getenv(strings.ToUpper(e.Name) + "_" + "PASSWORD"); len(password) > 0 {
            config.Hosts[i].Password = password
        } else if len(h.Password) == 0 {
            return nil, fmt.Errorf("password not defined for %s", e.Name)
        }

        fmt.Printf("Hostname: %s", h.name)
        fmt.Printf("Username: %s", h.Username)
        fmt.Printf("Password: %s", h.Password)
    }
}

例如,我首先检查环境变量HOST1_USERNAME1HOST1_PASSWORD1HOST2_USERNAME2HOST2_PASSWORD2是否存在...如果存在,我将配置项设置为它们的值,否则我尝试从配置文件中获取值。

我知道viper提供了AutomaticEnv方法来实现类似的功能...但是它是否适用于像我这样的集合(AutomaticEnv应该在环境变量绑定之后调用)?

根据我上面的代码,是否可以使用viper提供的机制并删除os.GetEnv

谢谢。

更新

以下是我的更新后的代码。我定义了环境变量HOST1_USERNAMEHOST1_PASSWORD,并将配置文件中相应的设置设置为空字符串。

这是我的新配置文件:

[host1]
username = ""
password = ""

[host2]
username = "user2"
password = "password2"

这是我的代码:

package config

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

type Config struct {
    Hosts []Host
}

type Host struct {
    Name     string
    Username string
    Password string
}

var config *Config

func (c *Config) Load() (*Config, error) {
    if config == nil {
        viper.AddConfigPath(".")
        viper.AddConfigPath("./config")
        viper.SetConfigName("myapp")
        viper.AutomaticEnv()
        viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

        if err := viper.ReadInConfig(); err != nil {
            return nil, fmt.Errorf("error reading config file, %v", err)
        }

        allSettings := viper.AllSettings()
        hosts := make([]Host, 0, len(allSettings))

        for key, value := range allSettings {
            val := value.(map[string]interface{})

            if val["username"] == nil {
                return nil, fmt.Errorf("username not defined for host %s", key)
            }

            if val["password"] == nil {
                return nil, fmt.Errorf("password not defined for host %s", key)
            }

            hosts = append(hosts, Host{
                Name:      key,
                Unsername: val["username"].(string),
                Password: val["password"].(string),
            })
        }

        config = &Config{hosts}
    }

    return config, nil
}

现在它可以工作了(感谢Chrono Kitsune),希望对你有所帮助。

英文:

Here below is my configuration file in toml format.

[[hosts]]
name = "host1"
username = "user1"
password = "password1"
[[hosts]]
name = "host2"
username = "user2"
password = "password2"

... and here is my code to load it:

import (
"fmt"
"github.com/spf13/viper"
"strings"
)
type Config struct {
Hosts []Host
}
type Host struct {
Name      string `mapstructure:"name"`
Username  string `mapstructure:"username"`
Password  string `mapstructure:"password"`
}
func main() {
viper.AddConfigPath(".")
viper.AddConfigPath("./config")
viper.SetConfigName("app")
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("error reading config file, %v", err)
}
config := new(Config)
if err := viper.Unmarshal(config); err != nil {
return nil, fmt.Errorf("error parsing config file, %v", err)
}
var username, password string
for i, h := range config.Hosts {
if len(h.Name) == 0 {
return nil, fmt.Errorf("name not defined for host %d", i)
}
if username = os.Getenv(strings.ToUpper(h.Name) + "_" + "USERNAME"); len(username) > 0 {
config.Hosts[i].Username = username
} else if len(h.Username) == 0 {
return nil, fmt.Errorf("username not defined for %s", e.Name)
}
if password = os.Getenv(strings.ToUpper(e.Name) + "_" + "PASSWORD"); len(password) > 0 {
config.Hosts[i].Password = password
} else if len(h.Password) == 0 {
return nil, fmt.Errorf("password not defined for %s", e.Name)
}
fmt.Printf("Hostname: %s", h.name)
fmt.Printf("Username: %s", h.Username)
fmt.Printf("Password: %s", h.Password)
}
}

For instance, I first check whether environment variables HOST1_USERNAME1, HOST1_PASSWORD1, HOST2_USERNAME2, and HOST2_PASSWORD2 exist... if they do, then I set the configuration items to their values, otherwise I try to get the values from the configuration file.

I know viper offers method AutomaticEnv to do something similar... but does it work with a collection like mine (AutomaticEnv shall be invoked after environment variable binding)?

Given my code above, is it possible to use the mechanism provided by viper and remove os.GetEnv?

Thanks.

UPDATE

Here below is my updated code. In I've defined environment variables HOST1_USERNAME and HOST1_PASSWORD and set the corresponding settings in my config file to an empty string.

Here is my new config file:

[host1]
username = ""
password = ""
[host2]
username = "user2"
password = "password2"

And here is my code:

package config
import (
"fmt"
"github.com/spf13/viper"
"strings"
"sync"
)
type Config struct {
Hosts []Host
}
type Host struct {
Name     string
Username string
Password string
}
var config *Config
func (c *Config) Load() (*Config, error) {
if config == nil {
viper.AddConfigPath(".")
viper.AddConfigPath("./config")
viper.SetConfigName("myapp")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("error reading config file, %v", err)
}
allSettings := viper.AllSettings()
hosts := make([]Host, 0, len(allSettings))
for key, value := range allSettings {
val := value.(map[string]interface{})
if val["username"] == nil {
return nil, fmt.Errorf("username not defined for host %s", key)
}
if val["password"] == nil {
return nil, fmt.Errorf("password not defined for host %s", key)
}
hosts = append(hosts, Host{
Name:      key,
Unsername: val["username"].(string),
Password: val["password"].(string),
})
}
config = &Config{hosts}
}
return config, nil
}

I works now (thanks to Chrono Kitsune) and I hope it helps,
j3d

答案1

得分: 1

viper.Viper

> 源的优先级如下:
>
> 1. 覆盖
> 2. 标志
> 3. 环境变量
> 4. 配置文件
> 5. 键/值存储
> 6. 默认值

您可能会遇到一个问题,即确定环境变量的名称。您实际上需要一种将hosts[0].Username绑定到环境变量HOST1_USERNAME的方法。然而,目前在viper中没有办法做到这一点。实际上,viper.Get("hosts[0].username")返回nil,这意味着数组索引显然不能与viper.BindEnv一起使用。您还需要为定义的每个主机使用此函数,这意味着如果列出了20个主机,您需要调用viper.BindEnv 40或60次,具体取决于主机的名称是否可以被环境变量覆盖。为了解决这个限制,您需要将主机作为独立的表而不是表数组进行动态处理:

[host1]
username = "user1"
password = "password1"
[host2]
username = "user2"
password = "password2"

然后,您可以使用viper.SetEnvKeyReplacerstrings.Replacer来处理环境变量问题:

// host1.username => HOST1_USERNAME
// host2.password => HOST2_PASSWORD
// 等等。
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

请注意,在撰写本文时,在解析顺序方面存在一些错误。这个问题影响到viper.Unmarshalviper.Get:环境变量应该覆盖配置文件中的值,但目前仍然使用配置文件中的值。奇怪的是,viper.AllSettings工作正常。如果不是这样,您无法像上面的格式一样处理主机:

// 手动收集主机以存储在配置中。
func collectHosts() []Host {
hosts := make([]Host, 0, 10)
for key, value := range viper.AllSettings() {
// viper.GetStringMapString(key)
// 在解决
//   https://github.com/spf13/viper/issues/309
// 之前,无法正确处理环境变量等。
v := value.(map[string]interface{})
hosts = append(hosts, Host{
Name:     key,
Username: v["username"].(string),
Password: v["password"].(string),
})
}
return hosts
}

总结一下:

  1. 值应该从提供的第一个源中获取:覆盖、标志、环境变量、配置文件、键/值存储、默认值。不幸的是,这并不总是遵循的顺序(因为存在错误)。
  2. 您需要更改配置格式并使用字符串替换器来利用viper.AutomaticEnv的便利性。
英文:

From viper.Viper:

> The priority of the sources is the following:
>
> 1. overrides
> 2. flags
> 3. env. variables
> 4. config file
> 5. key/value store
> 6. defaults

You might encounter a problem in determining the name of the environment variable. You essentially need a way to bind hosts[0].Username to the environment variable HOST1_USERNAME. However, there's no way to do this in viper currently. In fact, viper.Get("hosts[0].username") returns nil, meaning array indices apparently cannot be used with viper.BindEnv. You also would need to use this function for as many hosts as can be defined, meaning if you have 20 hosts listed, you'd need to call viper.BindEnv 40 or 60 times, depending on whether the name of a host could be overridden by an environment variable. To work around this limitation, you'd need to dynamically work with hosts as independent tables rather than an array of tables:

[host1]
username = "user1"
password = "password1"
[host2]
username = "user2"
password = "password2"

You can then use viper.SetEnvKeyReplacer with a strings.Replacer to handle the environment variable issue:

// host1.username => HOST1_USERNAME
// host2.password => HOST2_PASSWORD
// etc.
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

Note that at the time of this writing some bugs exist when it comes to the resolution order. This issue affects viper.Unmarshal and viper.Get: environment variables should override config file values, but currently the config file values are still used. Strangely, viper.AllSettings works fine. If it didn't, you couldn't do something like the following to work with the hosts in the above format:

// Manually collect hosts for storing in config.
func collectHosts() []Host {
hosts := make([]Host, 0, 10)
for key, value := range viper.AllSettings() {
// viper.GetStringMapString(key)
// won't work properly with environment vars, etc. until
//   https://github.com/spf13/viper/issues/309
// is resolved.
v := value.(map[string]interface{})
hosts = append(hosts, Host{
Name:     key,
Username: v["username"].(string),
Password: v["password"].(string),
})
}
return hosts
}

To sum things up:

  1. Values are supposed to be taken from the first provided of: overrides, flags, environment variables, config files, key/value stores, defaults. Unfortunately, this isn't always the order followed (because bugs).
  2. You need to change your config format and use a string replacer to leverage the convenience of viper.AutomaticEnv.

huangapple
  • 本文由 发表于 2017年9月2日 05:42:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/46008299.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定