diff --git a/cmd/relayproxy/config/config.go b/cmd/relayproxy/config/config.go index 91289ff8233..5e52de0f5ea 100644 --- a/cmd/relayproxy/config/config.go +++ b/cmd/relayproxy/config/config.go @@ -2,12 +2,6 @@ package config import ( "fmt" - "net/http" - "os" - "path/filepath" - "strings" - "time" - "github.com/knadh/koanf/parsers/json" "github.com/knadh/koanf/parsers/toml" "github.com/knadh/koanf/parsers/yaml" @@ -19,6 +13,12 @@ import ( "github.com/spf13/pflag" "github.com/xitongsys/parquet-go/parquet" "go.uber.org/zap" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" ) var k = koanf.New(".") @@ -91,10 +91,18 @@ func New(flagSet *pflag.FlagSet, log *zap.Logger, version string) (*Config, erro log.Error("error loading file", zap.Error(errBindFile)) } } - // Map environment variables - _ = k.Load(env.Provider("", ".", func(s string) string { - return strings.ReplaceAll(strings.ToLower(s), "_", ".") + _ = k.Load(env.ProviderWithValue("", ".", func(s string, v string) (string, interface{}) { + if strings.HasPrefix(s, "RETRIEVERS") || strings.HasPrefix(s, "NOTIFIERS") { + configMap := k.Raw() + err := loadArrayEnv(s, v, configMap) + if err != nil { + log.Error("config: error loading array env", zap.String("key", s), zap.String("value", v), zap.Error(err)) + return s, v + } + return s, v + } + return strings.ReplaceAll(strings.ToLower(s), "_", "."), v }), nil) _ = k.Set("version", version) @@ -322,3 +330,60 @@ func locateConfigFile(inputFilePath string) (string, error) { return "", fmt.Errorf( "impossible to find config file in the default locations [%s]", strings.Join(defaultLocations, ",")) } + +// Load the ENV Like:RETRIEVERS_0_HEADERS_AUTHORIZATION +func loadArrayEnv(s string, v string, configMap map[string]interface{}) error { + paths := strings.Split(s, "_") + for i, str := range paths { + paths[i] = strings.ToLower(str) + } + prefixKey := paths[0] + if configArray, ok := configMap[prefixKey].([]interface{}); ok { + index, err := strconv.Atoi(paths[1]) + if err != nil { + return err + } + var configItem map[string]interface{} + outRange := index > len(configArray)-1 + if outRange { + configItem = make(map[string]interface{}) + } else { + configItem = configArray[index].(map[string]interface{}) + } + + keys := paths[2:] + currentMap := configItem + for i, key := range keys { + hasKey := false + lowerKey := key + for y := range currentMap { + if y != lowerKey { + continue + } + if nextMap, ok := currentMap[y].(map[string]interface{}); ok { + currentMap = nextMap + hasKey = true + break + } + } + if !hasKey && i != len(keys)-1 { + newMap := make(map[string]interface{}) + currentMap[lowerKey] = newMap + currentMap = newMap + } + } + lastKey := keys[len(keys)-1] + currentMap[lastKey] = v + if outRange { + blank := index - len(configArray) + 1 + for i := 0; i < blank; i++ { + configArray = append(configArray, make(map[string]interface{})) + } + configArray[index] = configItem + } else { + configArray[index] = configItem + } + _ = k.Set(prefixKey, configArray) + } + return nil +} diff --git a/cmd/relayproxy/config/config_test.go b/cmd/relayproxy/config/config_test.go index f375fd94a26..8a8870bb499 100644 --- a/cmd/relayproxy/config/config_test.go +++ b/cmd/relayproxy/config/config_test.go @@ -692,3 +692,104 @@ func TestConfig_APIAdminKeyExists(t *testing.T) { }) } } + +func TestMergeConfig_FromOSEnv(t *testing.T) { + tests := []struct { + name string + want *config.Config + fileLocation string + wantErr assert.ErrorAssertionFunc + disableDefaultFileCreation bool + }{ + { + name: "Valid file", + fileLocation: "../testdata/config/validate-array-env-file.yaml", + want: &config.Config{ + ListenPort: 1031, + PollingInterval: 1000, + FileFormat: "yaml", + Host: "localhost", + Retrievers: &[]config.RetrieverConf{ + config.RetrieverConf{ + Kind: "http", + URL: "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/examples/retriever_file/flags.goff.yaml", + HTTPHeaders: map[string][]string{ + "authorization": []string{ + "test", + }, + "token": []string{"token"}, + }, + }, + config.RetrieverConf{ + Kind: "file", + Path: "examples/retriever_file/flags.goff.yaml", + HTTPHeaders: map[string][]string{ + "token": []string{ + "11213123", + }, + "authorization": []string{ + "test1", + }, + }, + }, + config.RetrieverConf{ + HTTPHeaders: map[string][]string{ + + "authorization": []string{ + "test1", + }, + "x-goff-custom": []string{ + "custom", + }, + }, + }, + }, + Exporter: &config.ExporterConf{ + Kind: "log", + }, + StartWithRetrieverError: false, + RestAPITimeout: 5000, + Version: "1.X.X", + EnableSwagger: true, + AuthorizedKeys: config.APIKeys{ + Admin: []string{ + "apikey3", + }, + Evaluation: []string{ + "apikey1", + "apikey2", + }, + }, + }, + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + os.Setenv("RETRIEVERS_0_HEADERS_AUTHORIZATION", "test") + os.Setenv("RETRIEVERS_X_HEADERS_AUTHORIZATION", "test") + os.Setenv("RETRIEVERS_1_HEADERS_AUTHORIZATION", "test1") + os.Setenv("RETRIEVERS_0_HEADERS_TOKEN", "token") + os.Setenv("RETRIEVERS_2_HEADERS_AUTHORIZATION", "test1") + os.Setenv("RETRIEVERS_2_HEADERS_X-GOFF-CUSTOM", "custom") + t.Run(tt.name, func(t *testing.T) { + _ = os.Remove("./goff-proxy.yaml") + if !tt.disableDefaultFileCreation { + source, _ := os.Open(tt.fileLocation) + destination, _ := os.Create("./goff-proxy.yaml") + defer destination.Close() + defer source.Close() + defer os.Remove("./goff-proxy.yaml") + _, _ = io.Copy(destination, source) + } + + f := pflag.NewFlagSet("config", pflag.ContinueOnError) + f.String("config", "", "Location of your config file") + _ = f.Parse([]string{fmt.Sprintf("--config=%s", tt.fileLocation)}) + got, err := config.New(f, zap.L(), "1.X.X") + if !tt.wantErr(t, err) { + return + } + assert.Equal(t, tt.want, got, "Config not matching") + }) + } +} diff --git a/cmd/relayproxy/testdata/config/validate-array-env-file.yaml b/cmd/relayproxy/testdata/config/validate-array-env-file.yaml new file mode 100644 index 00000000000..faff1bd8197 --- /dev/null +++ b/cmd/relayproxy/testdata/config/validate-array-env-file.yaml @@ -0,0 +1,19 @@ +listen: 1031 +pollingInterval: 1000 +startWithRetrieverError: false +retrievers: + - kind: http + url: https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/examples/retriever_file/flags.goff.yaml + - kind: file + path: examples/retriever_file/flags.goff.yaml + headers: + token: 11213123 +exporter: + kind: log +enableSwagger: true +authorizedKeys: + evaluation: + - apikey1 # owner: userID1 + - apikey2 # owner: userID2 + admin: + - apikey3