Not today...

comments

Snippet

Viper Multi Config

Tagged golang , dev , cli

This post will explain how to overload configurations in Golang using viper and cobra libraries. The use case may be a niche one, but I find this to be an easy to understand and pretty clear way to do configuration merge.

The problem and the expectation

I am deploying most of my application on Kubernetes nowadays (I also moved my personal infrastructure to it a few weeks ago). This solution come with a big tooling ecosystem and a really opiniated way to deploy.

Most of the time applications you deploy come with a default configuration stored in a config map (using Kustomize, Helm, ….). My problem was that I wanted to package one of my application with a default configuration, then overload it only for specific entries. Here is an example to make this a bit more clear.

[log]
level = "info"
type = "json"

[sql]
host = "localhost"
port = 3306

My application come packaged with a configuration file stored in a config map. Let’s imagine I want to only set a different log level for one of my deployment. I would like to avoid doing this through the command line since some entries may be tricky to map to a proper option.

The idea would be to keep this config map, and create a second one with only the entries I want to modify. For example:

[log]
level = "debug"

[sql]
host = "my.remote.endpoint"

In memory my application will merge those two files to create a final configuration looking like this:

[log]
level = "debug"
type = "json"

[sql]
host = "my.remote.endpoint"
port = 3306

My application will then be called this way: myapp --config=config_1.toml --config=config_2.toml. It will load config files in order overriding the config entries as it goes and keeping the base one if they are not modified.

The implementation

package main

import (
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

type Configuration struct {
	Logger        struct {
		Level string `mapstructure:"level"`
		Type  string `mapstructure:"type"`
	} `mapstructure:"log"`
	SQL struct {
		Host string `mapstructure:"database"`
		Port int    `mapstructure:"table"`
	} `mapstructure:"sql"`
}

func init() {
	flags := command.PersistentFlags()
	flags.StringArray("config", []string{}, "Path to configuration files")
}

func config(cmd *cobra.Command) (*Configuration, error) {
	configuration := &Configuration{}
	configs, err := cmd.PersistentFlags().GetStringArray("config")
	if err != nil {
		return nil, err
	}
	for _, config := range configs {
		viper.SetConfigFile(config)
		if err := viper.MergeInConfig(); err != nil {
			return nil, err
		}
	}

	return configuration, viper.Unmarshal(configuration)
}

var (
	command = &cobra.Command{
		Use:   "myapp [flags]",
		RunE: func(cmd *cobra.Command, args []string) error {
			// https://github.com/spf13/cobra/issues/340
			cmd.SilenceUsage = true
			config, err := config(cmd)
			if err != nil {
				return err
			}
			// ...
		},
	}
)

Here the magic is happening in the config function. We are using the global viper object and we push the entries of our option. Then we call the MergeInConfig function and it will perform what we were looking for.

This is pretty straightforward but a bit hidden in the doc so not really obvious. Hope it helps someone out there, I find this feature pretty convenient and I will most probably integrate it in my upcoming developments.