英文:
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_USERNAME1
、HOST1_PASSWORD1
、HOST2_USERNAME2
和HOST2_PASSWORD2
是否存在...如果存在,我将配置项设置为它们的值,否则我尝试从配置文件中获取值。
我知道viper提供了AutomaticEnv
方法来实现类似的功能...但是它是否适用于像我这样的集合(AutomaticEnv
应该在环境变量绑定之后调用)?
根据我上面的代码,是否可以使用viper提供的机制并删除os.GetEnv
?
谢谢。
更新
以下是我的更新后的代码。我定义了环境变量HOST1_USERNAME
和HOST1_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
> 源的优先级如下:
>
> 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.SetEnvKeyReplacer
和strings.Replacer
来处理环境变量问题:
// host1.username => HOST1_USERNAME
// host2.password => HOST2_PASSWORD
// 等等。
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
请注意,在撰写本文时,在解析顺序方面存在一些错误。这个问题影响到viper.Unmarshal
和viper.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
}
总结一下:
- 值应该从提供的第一个源中获取:覆盖、标志、环境变量、配置文件、键/值存储、默认值。不幸的是,这并不总是遵循的顺序(因为存在错误)。
- 您需要更改配置格式并使用字符串替换器来利用
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:
- 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).
- You need to change your config format and use a string replacer to leverage the convenience of
viper.AutomaticEnv
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论