Viper
I’m starting to think that using viper to manage configuration and command line arguments is advantageous for the following reasons
- It allows configuration to be loaded from environment variables, files, or command line flags
- viper handles merging all those sources and overriding them
- As the number of options gets large you can switch to using a file
- Makes it easy for CLIs to persist them to a configuration file
- In Kubernetes deployments you can load them from a configmap
- If you use a file a flat structure then you can just store the values in the config map and use kustomize to override values
Patterns
I’m still working out the best patterns for using viper but here’s what I’m thinking so far (look at .kubedr for an example)
Define a root level persistent --config
flag to allow the config file to be overwritten via a command line argument. e.g.
func newRootCmd() *cobra.Command {
...
rootCmd.PersistentFlags().StringVar(&cfgFile, config.ConfigFlagName, "", "config file (default is $HOME/.kubedr/config.yaml)")
}
There are three things we need to define to use a Viper configuration
func Init(cmd *cobra.command)
type Configuration struct
func GetConfig()
Init function
The init function initializes the viper configuration e.g.
func InitViper(cmd *cobra.Command) error {
// TODO: I think you want to set any defaults.
viper.SetEnvPrefix("kubedr")
viper.SetConfigName("config") // name of config file (without extension)
viper.AddConfigPath("$HOME/.kubedr") // adding home directory as first search path
viper.AutomaticEnv() // read in environment variables that match
// Bind to the command line flag if it was specified.
// N.B. I think the key name can be a dotted name if you want to store the key in a nested dictionary
// when serializing to a file.
if err := viper.BindPFlag(ConfigFlagName, cmd.Flags().Lookup(ConfigFlagName)); err != nil {
return err
}
// We want to make sure the config file path gets set.
// This is because we use viper to persist the location of the config file so can save to it.
cfgFile := viper.GetString(ConfigFlagName)
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
}
err := viper.ReadInConfig()
return err
}
The init function determines the following
- Name of the configuration file
- Search path for configuration file
- Binding of any command line flags to viper variables
- This is why the cmd is passed in.
Configuration struct
Define a GoLang struct store the configuration
type Config struct {
APIVersion string `json:"apiVersion" yaml:"apiVersion" yamltags:"required"`
Kind string `json:"kind" yaml:"kind" yamltags:"required"`
// APIKeyFile is the path to the file containing the API key.
// Can be a URI like gcpsecretmanager:/// to point to a secret in GCP secret manager
APIKeyFile string `json:"apiKeyFile" yaml:"apiKeyFile"`
}
This is the struct that should get passed around to actual functions that need the configuration. Functions should not access viper directly.
GetConfig
Define a GetConfig
function that will create your configuration object with values loaded from viper. You can use Unmarshal to decode it into a typed object.
// GetConfig returns the configuration instantiatiated from the viper configuration.
func GetConfig() *Config {
cfg := &Config{}
if err := viper.Unmarshal(cfg); err != nil {
panic(fmt.Errorf("Failed to unmarshal configuration; error %v", err))
}
}
TODO(jeremy): Can we just use Viper to serialize to JSON and then desierialize to Config?
Call Init and GetConfig
Inside your actual command call your Init
and GetConfig
functions e.g.
func NewRunCmd() *cobra.Command {
var namespace string
var opts kubedr.Options
cmd := &cobra.Command{
Use: "diagnose <noun> <name>",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
if err := config.InitViper(cmd); err != nil {
return err
}
cfg := config.GetConfig()
...
}
...
}
...
}
Flags
TODO(jeremy): If user doesn’t specify a flag but the flag has a default value what happens? In particular if the value is set int he configuration file with the flags default value override the value in the configuration file?
Nested Fields
See https://github.com/spf13/viper
Testing overrides
I’m not sure if there is a good way to test viper configurations. The problem is if you want to set environment variables in the configuration you need to actually change the environment.
I’m also not sure how to set arguments via command line flags without calling execute. Calling cmd.SetArgs
isn’t enough.