diff --git a/fig.go b/fig.go index 510a4f4..f6590c5 100644 --- a/fig.go +++ b/fig.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/imdario/mergo" "github.com/mitchellh/mapstructure" "github.com/pelletier/go-toml" "gopkg.in/yaml.v2" @@ -76,6 +77,8 @@ type fig struct { timeLayout string useEnv bool envPrefix string + useProfile bool + profile string } func (f *fig) Load(cfg interface{}) error { @@ -93,6 +96,22 @@ func (f *fig) Load(cfg interface{}) error { return err } + if f.useProfile { + profileFile, err := f.findProfileCfgFile() + if err != nil { + return err + } + + profileVals, err := f.decodeFile(profileFile) + if err != nil { + return err + } + + if err := mergo.Merge(&vals, profileVals, mergo.WithOverride); err != nil { + return err + } + } + if err := f.decodeMap(vals, cfg); err != nil { return err } @@ -100,6 +119,18 @@ func (f *fig) Load(cfg interface{}) error { return f.processCfg(cfg) } +func (f *fig) findProfileCfgFile() (path string, err error) { + parts := strings.Split(f.filename, ".") + profileFileName := fmt.Sprintf("%s.%s.%s", parts[0], f.profile, parts[1]) + for _, dir := range f.dirs { + path = filepath.Join(dir, profileFileName) + if fileExists(path) { + return + } + } + return "", fmt.Errorf("%s: %w", profileFileName, ErrFileNotFound) +} + func (f *fig) findCfgFile() (path string, err error) { for _, dir := range f.dirs { path = filepath.Join(dir, f.filename) @@ -151,6 +182,7 @@ func (f *fig) decodeMap(m map[string]interface{}, result interface{}) error { Result: result, TagName: f.tag, DecodeHook: mapstructure.ComposeDecodeHookFunc( + fromEnvironmentHookFunc(), mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(f.timeLayout), ), @@ -161,6 +193,31 @@ func (f *fig) decodeMap(m map[string]interface{}, result interface{}) error { return dec.Decode(m) } +func fromEnvironmentHookFunc() mapstructure.DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + + str := data.(string) + if !strings.HasPrefix(str, "${") || !strings.HasSuffix(str, "}") { + return data, nil + } + + kv := str[2 : len(str)-1] + s := strings.Split(kv, ":") + envName, defaultVal := s[0], s[1] + if envValue, ok := os.LookupEnv(envName); ok { + return envValue, nil + } else { + return defaultVal, nil + } + } +} + // processCfg processes a cfg struct after it has been loaded from // the config file, by validating required fields and setting defaults // where applicable. diff --git a/fig_test.go b/fig_test.go index 35c3826..b67abd7 100644 --- a/fig_test.go +++ b/fig_test.go @@ -154,6 +154,26 @@ func Test_fig_Load(t *testing.T) { } } +func Test_fig_Load_If_Env_Set_In_Conf_File(t *testing.T) { + os.Setenv("POD_NAME", "ehcache") + for _, f := range []string{"pod.yaml", "pod.json", "pod.toml"} { + t.Run(f, func(t *testing.T) { + var cfg Pod + err := Load(&cfg, File(f), Dirs(filepath.Join("testdata", "valid"))) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + want := validPodConfig() + want.Metadata.Name = "ehcache" + + if !reflect.DeepEqual(want, cfg) { + t.Errorf("\nwant %+v\ngot %+v", want, cfg) + } + }) + } +} + func Test_fig_Load_FileNotFound(t *testing.T) { fig := defaultFig() fig.filename = "abrakadabra" @@ -402,6 +422,55 @@ func Test_fig_Load_WithOptions(t *testing.T) { } } +func Test_fig_Load_Server_If_Env_Set_In_Conf_File(t *testing.T) { + os.Setenv("SERVICE_HOST", "192.168.0.128") + for _, f := range []string{"server.yaml", "server.json", "server.toml"} { + t.Run(f, func(t *testing.T) { + type Server struct { + Host string `fig:"host"` + } + + var cfg Server + err := Load(&cfg, File(f), Dirs(filepath.Join("testdata", "valid"))) + if err != nil { + t.Fatalf("expected err") + } + + want := Server{Host: "192.168.0.128"} + + if !reflect.DeepEqual(want, cfg) { + t.Errorf("\nwant %+v\ngot %+v", want, cfg) + } + }) + } +} + +func Test_fig_Load_Server_Profile(t *testing.T) { + for _, f := range []string{"server.yaml", "server.json", "server.toml"} { + t.Run(f, func(t *testing.T) { + type Server struct { + Host string `fig:"host"` + Logger struct { + LogLevel string `fig:"log_level" default:"info"` + } + } + + var cfg Server + err := Load(&cfg, File(f), Dirs(filepath.Join("testdata", "valid")), UseProfile("test")) + if err != nil { + t.Fatalf("expected err %v", err) + } + + want := Server{Host: "192.168.0.256"} + want.Logger.LogLevel = "debug" + + if !reflect.DeepEqual(want, cfg) { + t.Errorf("\nwant %+v\ngot %+v", want, cfg) + } + }) + } +} + func Test_fig_findCfgFile(t *testing.T) { t.Run("finds existing file", func(t *testing.T) { fig := defaultFig() diff --git a/go.mod b/go.mod index 5cff629..e939ae3 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module github.com/kkyr/fig go 1.14 require ( + github.com/imdario/mergo v0.3.12 github.com/mitchellh/mapstructure v1.1.2 github.com/pelletier/go-toml v1.6.0 - gopkg.in/yaml.v2 v2.2.7 + gopkg.in/yaml.v2 v2.3.0 ) diff --git a/go.sum b/go.sum index 34d6acb..6d458fa 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= @@ -11,3 +13,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/option.go b/option.go index 0adbbf2..5551e3b 100644 --- a/option.go +++ b/option.go @@ -94,3 +94,16 @@ func UseEnv(prefix string) Option { f.envPrefix = prefix } } + +// Tag returns an option that configures the tag key that fig uses +// when for the alt name struct tag key in fields. +// +// fig.Load(&cfg, fig.UseProfile("test")) +// +// If this option is not used then fig uses the tag `fig`. +func UseProfile(profile string) Option { + return func(f *fig) { + f.useProfile = true + f.profile = profile + } +} diff --git a/testdata/valid/pod.json b/testdata/valid/pod.json index eeafa74..57edeab 100644 --- a/testdata/valid/pod.json +++ b/testdata/valid/pod.json @@ -2,7 +2,7 @@ "apiVersion": null, "kind": "Pod", "metadata": { - "name": "redis", + "name": "${POD_NAME:redis}", "master": true }, "spec": { diff --git a/testdata/valid/pod.toml b/testdata/valid/pod.toml index dd7b9a2..c82cf05 100644 --- a/testdata/valid/pod.toml +++ b/testdata/valid/pod.toml @@ -1,7 +1,7 @@ kind = "Pod" [metadata] -name = "redis" +name = "${POD_NAME:redis}" master = true [spec] diff --git a/testdata/valid/pod.yaml b/testdata/valid/pod.yaml index 3919442..904e9d2 100644 --- a/testdata/valid/pod.yaml +++ b/testdata/valid/pod.yaml @@ -1,7 +1,7 @@ apiVersion: kind: Pod metadata: - name: redis + name: ${POD_NAME:redis} master: true spec: containers: diff --git a/testdata/valid/server.json b/testdata/valid/server.json index e7edd71..2366a5e 100644 --- a/testdata/valid/server.json +++ b/testdata/valid/server.json @@ -1,5 +1,5 @@ { - "host": "0.0.0.0", + "host": "${SERVICE_HOST:0.0.0.0}", "logger": { "log_level": "debug" } diff --git a/testdata/valid/server.test.json b/testdata/valid/server.test.json new file mode 100644 index 0000000..6fda411 --- /dev/null +++ b/testdata/valid/server.test.json @@ -0,0 +1,3 @@ +{ + "host": "192.168.0.256" +} \ No newline at end of file diff --git a/testdata/valid/server.test.toml b/testdata/valid/server.test.toml new file mode 100644 index 0000000..1247270 --- /dev/null +++ b/testdata/valid/server.test.toml @@ -0,0 +1 @@ +host = "192.168.0.256" \ No newline at end of file diff --git a/testdata/valid/server.test.yaml b/testdata/valid/server.test.yaml new file mode 100644 index 0000000..414070c --- /dev/null +++ b/testdata/valid/server.test.yaml @@ -0,0 +1 @@ +host: "192.168.0.256" \ No newline at end of file diff --git a/testdata/valid/server.toml b/testdata/valid/server.toml index a5ba440..d756bd1 100644 --- a/testdata/valid/server.toml +++ b/testdata/valid/server.toml @@ -1,4 +1,4 @@ -host = "0.0.0.0" +host = "${SERVICE_HOST:0.0.0.0}" [logger] log_level = "debug" \ No newline at end of file diff --git a/testdata/valid/server.yaml b/testdata/valid/server.yaml index c6ddab8..30df7be 100644 --- a/testdata/valid/server.yaml +++ b/testdata/valid/server.yaml @@ -1,4 +1,4 @@ -host: "0.0.0.0" +host: "${SERVICE_HOST:0.0.0.0}" logger: log_level: "debug" \ No newline at end of file