diff --git a/internal/cmdutils/bind-viper-to-flags.go b/internal/cmdutils/bind-viper-to-flags.go index 8f032851..7232e638 100644 --- a/internal/cmdutils/bind-viper-to-flags.go +++ b/internal/cmdutils/bind-viper-to-flags.go @@ -17,7 +17,10 @@ limitations under the License. package cmdutils import ( + "encoding/json" "fmt" + "reflect" + "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -31,8 +34,9 @@ func BindViperToFlags(cmd *cobra.Command, viperInstance *viper.Viper) { if !flag.Changed && viperInstance.IsSet(configName) { value := viperInstance.Get(configName) - err := cmd.Flags().Set(flag.Name, fmt.Sprintf("%v", value)) - cobra.CheckErr(err) + for _, strVal := range viperValueToStrings(value) { + cobra.CheckErr(cmd.Flags().Set(flag.Name, strVal)) + } } }) @@ -40,3 +44,32 @@ func BindViperToFlags(cmd *cobra.Command, viperInstance *viper.Viper) { BindViperToFlags(subcmd, viperInstance) } } + +// viperValueToStrings converts a viper config value to a slice of strings for +// pflag.Set calls. Slice values (e.g. from YAML lists) produce one string per +// element. Scalar strings that look like a JSON array (start with "[" and end +// with "]") are parsed as JSON to support multiple values via env vars, e.g. +// FGA_CUSTOM_HEADERS='["X-Foo: bar","X-Baz: qux"]'. Other scalars produce a +// single-element slice. +func viperValueToStrings(value any) []string { + reflectValue := reflect.ValueOf(value) + + if reflectValue.Kind() == reflect.Slice || reflectValue.Kind() == reflect.Array { + result := make([]string, 0, reflectValue.Len()) + for i := range reflectValue.Len() { + result = append(result, fmt.Sprintf("%v", reflectValue.Index(i).Interface())) + } + + return result + } + + str := fmt.Sprintf("%v", value) + if strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]") { + var parsed []string + if err := json.Unmarshal([]byte(str), &parsed); err == nil { + return parsed + } + } + + return []string{str} +} diff --git a/internal/cmdutils/bind-viper-to-flags_test.go b/internal/cmdutils/bind-viper-to-flags_test.go new file mode 100644 index 00000000..42c7123e --- /dev/null +++ b/internal/cmdutils/bind-viper-to-flags_test.go @@ -0,0 +1,98 @@ +/* +Copyright © 2023 OpenFGA + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmdutils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestViperValueToStrings(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + value any + expected []string + }{ + { + name: "scalar string produces single-element slice", + value: "X-Custom-Header: value1", + expected: []string{"X-Custom-Header: value1"}, + }, + { + name: "typed string slice produces one string per element", + value: []string{"X-Custom-Header: value1", "X-Request-ID: abc123"}, + expected: []string{"X-Custom-Header: value1", "X-Request-ID: abc123"}, + }, + { + name: "any slice produces one string per element", + value: []any{"X-Custom-Header: value1", "X-Request-ID: abc123"}, + expected: []string{"X-Custom-Header: value1", "X-Request-ID: abc123"}, + }, + { + name: "empty slice", + value: []any{}, + expected: []string{}, + }, + { + name: "JSON array string is parsed into multiple entries", + value: `["X-Foo: bar","X-Baz: qux"]`, + expected: []string{"X-Foo: bar", "X-Baz: qux"}, + }, + { + name: "JSON array with single element", + value: `["X-Foo: bar"]`, + expected: []string{"X-Foo: bar"}, + }, + { + name: "string starting with [ but not ending with ] is treated as scalar", + value: "[not-an-array", + expected: []string{"[not-an-array"}, + }, + { + name: "string ending with ] but not starting with [ is treated as scalar", + value: "not-an-array]", + expected: []string{"not-an-array]"}, + }, + { + name: "invalid JSON array is treated as scalar", + value: "[not valid json]", + expected: []string{"[not valid json]"}, + }, + { + name: "boolean value is stringified", + value: true, + expected: []string{"true"}, + }, + { + name: "integer value is stringified", + value: 42, + expected: []string{"42"}, + }, + } + + for _, test := range testcases { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + result := viperValueToStrings(test.value) + assert.Equal(t, test.expected, result) + }) + } +}