【问题标题】:Go viper .yaml values environment variables overrideGo viper .yaml 值环境变量覆盖
【发布时间】:2018-10-11 09:20:51
【问题描述】:

我正在尝试在 go 应用程序中添加 application.yaml 文件,其中包含我想用环境变量覆盖的 ${RMQ_HOST} 值。

application.yaml 我有:

rmq:
  test:
    host: ${RMQ_HOST}
    port: ${RMQ_PORT}

在我的装载机中,我有:

log.Println("Loading config...")
viper.SetConfigName("application")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AutomaticEnv()
err := viper.ReadInConfig()

我遇到的问题是${RMQ_HOST} 不会被我在环境变量中设置的值替换,并尝试使用此字符串连接到 RabbitMQ

amqp://test:test@${RMQ_HOST}:${RMQ_PORT}/test

而不是

amqp://test:test@test:test/test

【问题讨论】:

  • 为什么要使用环境变量时使用 yaml 文件?
  • 因为 yaml 文件值被映射到环境变量,避免了硬编码键,使其在多个部署环境中更具可扩展性。也就是说,试图实现spring boot的功能。
  • 那么我可能是您使用 viper 获取值的方式,您需要阅读文档以了解哪种方式适合您

标签: go viper-go


【解决方案1】:

Viper 没有能力为键/值对中的值保留占位符,所以我已经设法用这段代码 sn-p 解决了我的问题:

log.Println("Loading config...")
viper.SetConfigName("application")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
err := viper.ReadInConfig()
if err != nil {
    panic("Couldn't load configuration, cannot start. Terminating. Error: " + err.Error())
}
log.Println("Config loaded successfully...")
log.Println("Getting environment variables...")
for _, k := range viper.AllKeys() {
    value := viper.GetString(k)
    if strings.HasPrefix(value, "${") && strings.HasSuffix(value, "}") {
        viper.Set(k, getEnvOrPanic(strings.TrimSuffix(strings.TrimPrefix(value,"${"), "}")))
    }
}

func getEnvOrPanic(env string) string {
    res := os.Getenv(env)
    if len(res) == 0 {
        panic("Mandatory env variable not found:" + env)
    }
    return res
}

这将覆盖集合中的所有占位符。

【讨论】:

  • len(env) == 0 应该是len(res) == 0
【解决方案2】:

更新:

我用这个功能扩展了原生 yaml 解析器,并在 github 上发布了它。

用法:

type Config struct {
    Port     string   `yaml:"port"`
    RabbitMQ RabbitMQ `yaml:"rabbitmq"`
}

type RabbitMQ struct {
    Host     string `yaml:"host"`
    Port     string `yaml:"port"`
    Username string `yaml:"username"`
    Password string `yaml:"password"`
    Vhost    string `yaml:"vhost"`
}

func main() {
    var config Config
    file, err := ioutil.ReadFile("application.yaml")
    if err != nil {
        panic(err)
    }
    yaml.Unmarshal(file, &config)
    spew.Dump(config)
}

application.yaml 是这样的:

port: ${SERVER_PORT}
rabbitmq:
  host: ${RMQ_HOST}
  port: ${RMQ_PORT}
  username: ${RMQ_USERNAME}
  password: ${RMQ_PASSWORD}
  vhost: test

vhost 值将照常解析,而用“${”和“}”包围的所有内容都将替换为环境变量。

【讨论】:

  • 是否支持默认值?即:${RMQ_HOST:dsa}
【解决方案3】:

您可以在调用“Unmarshal”方法之前显式替换环境变量。假设配置存储在“Config”变量中,下面的代码 sn-p 应该可以工作。

log.Println("Loading config...")
viper.SetConfigName("application")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
    fmt.Fprintf("Error reading config file %s\n", err)
}

for _, k := range viper.AllKeys() {
    v := viper.GetString(k)
    viper.Set(k, os.ExpandEnv(v))
}

if err := viper.Unmarshal(&Config); err != nil {
    fmt.Fprintf("Unable to decode into struct %s\n", err)
}

【讨论】:

    【解决方案4】:

    我通过使用regexp首先替换ENV解决了类似的问题,这是我的解决方案:

    # config.yaml
    DB_URI: ${DB_USER}
    

    和 main.go:

    package main
    
    import (
        "fmt"
        "os"
        "regexp"
    
        "github.com/spf13/viper"
    )
    
    type DBCfg struct {
        DBURI string `mapstructure:"DB_URI"`
    }
    
    func main() {
        viper.SetConfigName("config")
        viper.SetConfigType("yaml")
        viper.AddConfigPath(".")
        viper.AutomaticEnv()
    
        if err := viper.ReadInConfig(); err != nil {
            panic(fmt.Errorf("Failed to read config"))
        }
    
        for _, key := range viper.AllKeys() {
            value := viper.GetString(key)
            envOrRaw := replaceEnvInConfig([]byte(value))
            viper.Set(key, string(envOrRaw))
        }
    
        var config DBCfg
        if err := viper.Unmarshal(&config); err != nil {
            panic(fmt.Errorf("failed to load"))
        }
    
        fmt.Println(config)
    }
    
    func replaceEnvInConfig(body []byte) []byte {
        search := regexp.MustCompile(`\$\{([^{}]+)\}`)
        replacedBody := search.ReplaceAllFunc(body, func(b []byte) []byte {
            group1 := search.ReplaceAllString(string(b), `$1`)
    
            envValue := os.Getenv(group1)
            if len(envValue) > 0 {
                return []byte(envValue)
            }
            return []byte("")
        })
    
        return replacedBody
    }
    
    

    和我的输出:

    >>> DB_USER=iamddjsaio go run main.go
    
    {iamddjsaio}
    

    【讨论】:

    • 不是常见的解决方案,不适用于 map 等结构化配置值
    • 似乎该解决方案仅解析 env 值项,结构化配置值应在使用 viper 时按标签解析为您的自定义结构,而不是使用正则表达式。
    【解决方案5】:

    我认为更好的方法是在将配置解码为结构时使用 Viper 的 DecodeHooks 概念:

    如果你有如下的 yaml 配置文件:

    server:
      property: ${AN_ENV_VARIABLE}
    

    然后在解码钩子中,在解码发生时获取 ENV 变量:

    decodeHook := func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
        if f.Kind() == reflect.String {
            stringData := data.(string)
            if strings.HasPrefix(stringData, "${") && strings.HasSuffix(stringData, "}") {
                envVarValue := os.Getenv(strings.TrimPrefix(strings.TrimSuffix(stringData, "}"), "${"))
                if len(envVarValue) > 0 {
                    return envVarValue, nil
                }
            }
        }
        return data, nil
    }
    err := viper.Unmarshal(c, viper.DecodeHook(decodeHook))
    

    我认为这可以进一步改进,因为您可能有一个可能需要设置为 int 的传入属性,例如。但是,这个一般概念应该适用于大多数用例。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-01-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多