diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fd0f4f8515..8a175b92cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ 1. [18539](https://github.com/influxdata/influxdb/pull/18539): Collect stats on installed influxdata community template usage. 1. [18541](https://github.com/influxdata/influxdb/pull/18541): Pkger allow raw github.com host URLs for yaml|json|jsonnet URLs 1. [18546](https://github.com/influxdata/influxdb/pull/18546): Influx allow for files to be remotes for all template commands +1. [18568](https://github.com/influxdata/influxdb/pull/18568): Add support for config files to influxd and any cli.NewCommand use case ## v2.0.0-beta.12 [2020-06-12] diff --git a/kit/cli/viper.go b/kit/cli/viper.go index c22a9e635b7..902bc1b78a7 100644 --- a/kit/cli/viper.go +++ b/kit/cli/viper.go @@ -52,7 +52,7 @@ type Program struct { // // This is to simplify the viper/cobra boilerplate. func NewCommand(p *Program) *cobra.Command { - var cmd = &cobra.Command{ + cmd := &cobra.Command{ Use: p.Name, Args: cobra.NoArgs, RunE: func(_ *cobra.Command, _ []string) error { @@ -65,11 +65,37 @@ func NewCommand(p *Program) *cobra.Command { // This normalizes "-" to an underscore in env names. viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + if configFile := viper.GetString("CONFIG_FILE"); configFile != "" { + viper.SetConfigFile(configFile) + } else { + // defaults to looking in same directory as program running for + // a file `config.yaml` + viper.SetConfigFile("config.yaml") + } + + // done before we bind flags to viper keys. + // order of precedence (1 highest -> 3 lowest): + // 1. flags + // 2. env vars + // 3. config file + if err := initializeConfig(); err != nil { + panic(err) + } BindOptions(cmd, p.Opts) return cmd } +func initializeConfig() error { + err := viper.ReadInConfig() + if err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return err + } + } + return nil +} + // BindOptions adds opts to the specified command and automatically // registers those options with viper. func BindOptions(cmd *cobra.Command, opts []Opt) { diff --git a/kit/cli/viper_test.go b/kit/cli/viper_test.go index 3fbbd0738ec..de49af94c93 100644 --- a/kit/cli/viper_test.go +++ b/kit/cli/viper_test.go @@ -1,9 +1,15 @@ package cli import ( + "encoding/json" "fmt" + "io/ioutil" "os" + "path" + "testing" "time" + + "github.com/stretchr/testify/require" ) type customFlag bool @@ -101,3 +107,93 @@ func ExampleNewCommand() { // [foo bar] // on } + +func Test_NewProgram(t *testing.T) { + testFilePath, cleanup := newConfigFile(t, map[string]string{ + "FOO": "bar", + }) + defer cleanup() + defer setEnvVar("TEST_CONFIG_FILE", testFilePath)() + + tests := []struct { + name string + envVarVal string + args []string + expected string + }{ + { + name: "no vals reads from config", + expected: "bar", + }, + { + name: "reads from env var", + envVarVal: "foobar", + expected: "foobar", + }, + { + name: "reads from flag", + args: []string{"--foo=baz"}, + expected: "baz", + }, + { + name: "flag has highest precedence", + envVarVal: "foobar", + args: []string{"--foo=baz"}, + expected: "baz", + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + if tt.envVarVal != "" { + defer setEnvVar("TEST_FOO", tt.envVarVal)() + } + + var testVar string + program := &Program{ + Name: "test", + Opts: []Opt{ + { + DestP: &testVar, + Flag: "foo", + Required: true, + }, + }, + Run: func() error { return nil }, + } + + cmd := NewCommand(program) + cmd.SetArgs(append([]string{}, tt.args...)) + require.NoError(t, cmd.Execute()) + + require.Equal(t, tt.expected, testVar) + } + + t.Run(tt.name, fn) + } +} + +func setEnvVar(key, val string) func() { + old := os.Getenv(key) + os.Setenv(key, val) + return func() { + os.Setenv(key, old) + } +} + +func newConfigFile(t *testing.T, config interface{}) (string, func()) { + t.Helper() + + testDir, err := ioutil.TempDir("", "") + require.NoError(t, err) + + b, err := json.Marshal(config) + require.NoError(t, err) + + testFile := path.Join(testDir, "config.json") + require.NoError(t, ioutil.WriteFile(testFile, b, os.ModePerm)) + + return testFile, func() { + os.RemoveAll(testDir) + } +}