diff --git a/cmd/root.go b/cmd/root.go index 3bbc7d91..f28805e5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -18,7 +18,9 @@ limitations under the License. package cmd import ( + "errors" "fmt" + "io" "os" "strings" @@ -119,10 +121,7 @@ func initConfig() { viperInstance.SetConfigName(".fga") } - // If a config file is found, read it in. - if err := viperInstance.ReadInConfig(); err == nil { - fmt.Fprintln(os.Stderr, "Using config file:", viperInstance.ConfigFileUsed()) - } + readConfig(viperInstance, os.Stderr) viperInstance.SetEnvPrefix("FGA") viperInstance.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) @@ -131,3 +130,19 @@ func initConfig() { cmdutils.BindViperToFlags(rootCmd, viperInstance) } + +// readConfig reads the config file into viperInstance, if one is found, and reports on stderr which file is +// used. A config file that exists but can't be read, e.g. because of invalid YAML, is reported as a +// warning instead of being ignored silently. +func readConfig(viperInstance *viper.Viper, stderr io.Writer) { + err := viperInstance.ReadInConfig() + if err == nil { + fmt.Fprintln(stderr, "Using config file:", viperInstance.ConfigFileUsed()) + + return + } + + if !errors.As(err, &viper.ConfigFileNotFoundError{}) { + fmt.Fprintf(stderr, "Warning: ignoring config file %s: %v\n", viperInstance.ConfigFileUsed(), err) + } +} diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 00000000..100f0950 --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,98 @@ +package cmd + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadConfig(t *testing.T) { + t.Parallel() + + t.Run("valid config file", func(t *testing.T) { + t.Parallel() + + configFile := filepath.Join(t.TempDir(), ".fga.yaml") + require.NoError(t, os.WriteFile(configFile, []byte("api-url: https://api.fga.example\n"), 0o600)) + + viperInstance := viper.New() + viperInstance.SetConfigFile(configFile) + + var stderr bytes.Buffer + readConfig(viperInstance, &stderr) + + assert.Equal(t, "Using config file: "+configFile+"\n", stderr.String()) + assert.Equal(t, "https://api.fga.example", viperInstance.GetString("api-url")) + }) + + t.Run("invalid config file", func(t *testing.T) { + t.Parallel() + + configFile := filepath.Join(t.TempDir(), ".fga.yaml") + // YAML doesn't allow tabs for indentation + content := "api-url: https://api.fga.example\nstore-id: 01H0H015178Y2V4CX10C2KGHF4\n\tapi-token: token\n" + require.NoError(t, os.WriteFile(configFile, []byte(content), 0o600)) + + viperInstance := viper.New() + viperInstance.SetConfigFile(configFile) + + var stderr bytes.Buffer + readConfig(viperInstance, &stderr) + + assert.Contains(t, stderr.String(), "Warning: ignoring config file "+configFile+": ") + assert.Empty(t, viperInstance.GetString("api-url")) + }) + + t.Run("invalid config file found in the search paths", func(t *testing.T) { + t.Parallel() + + configDir := t.TempDir() + configFile := filepath.Join(configDir, ".fga.yaml") + // YAML doesn't allow tabs for indentation + content := "api-url: https://api.fga.example\nstore-id: 01H0H015178Y2V4CX10C2KGHF4\n\tapi-token: token\n" + require.NoError(t, os.WriteFile(configFile, []byte(content), 0o600)) + + viperInstance := viper.New() + viperInstance.AddConfigPath(configDir) + viperInstance.SetConfigName(".fga") + viperInstance.SetConfigType("yaml") + + var stderr bytes.Buffer + readConfig(viperInstance, &stderr) + + assert.Contains(t, stderr.String(), "Warning: ignoring config file "+configFile+": ") + assert.Empty(t, viperInstance.GetString("api-url")) + }) + + t.Run("config file given with --config doesn't exist", func(t *testing.T) { + t.Parallel() + + configFile := filepath.Join(t.TempDir(), "missing.fga.yaml") + + viperInstance := viper.New() + viperInstance.SetConfigFile(configFile) + + var stderr bytes.Buffer + readConfig(viperInstance, &stderr) + + assert.Contains(t, stderr.String(), "Warning: ignoring config file "+configFile+": ") + }) + + t.Run("no config file found", func(t *testing.T) { + t.Parallel() + + viperInstance := viper.New() + viperInstance.AddConfigPath(t.TempDir()) + viperInstance.SetConfigName(".fga") + + var stderr bytes.Buffer + readConfig(viperInstance, &stderr) + + assert.Empty(t, stderr.String()) + }) +}