From 83b42a04c7e3a813a9d10230255611e5918ef4ea Mon Sep 17 00:00:00 2001 From: William Petit Date: Fri, 24 May 2024 12:31:09 +0200 Subject: [PATCH] feat(config): interpolate recursively in interpolated map --- internal/admin/bootstrap.go | 4 +- internal/config/environment.go | 59 ++++++++++++---- internal/config/environment_test.go | 69 +++++++++++++++++++ internal/config/metrics.go | 4 +- .../environment/interpolated-map-1.yml | 6 ++ 5 files changed, 123 insertions(+), 19 deletions(-) create mode 100644 internal/config/environment_test.go create mode 100644 internal/config/testdata/environment/interpolated-map-1.yml diff --git a/internal/admin/bootstrap.go b/internal/admin/bootstrap.go index a39eefe..3b19921 100644 --- a/internal/admin/bootstrap.go +++ b/internal/admin/bootstrap.go @@ -65,7 +65,7 @@ func (s *Server) bootstrapProxies(ctx context.Context) error { for layerName, layerConfig := range proxyConfig.Layers { layerType := store.LayerType(layerConfig.Type) - layerOptions := store.LayerOptions(layerConfig.Options) + layerOptions := store.LayerOptions(layerConfig.Options.Data) if _, err := layerRepo.CreateLayer(ctx, proxyName, layerName, layerType, layerOptions); err != nil { return errors.WithStack(err) @@ -109,7 +109,7 @@ func (s *Server) validateBootstrap(ctx context.Context) error { } rawOptions := func(opts config.InterpolatedMap) map[string]any { - return opts + return opts.Data }(layerConf.Options) if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil { diff --git a/internal/config/environment.go b/internal/config/environment.go index be9bc56..4bc6f55 100644 --- a/internal/config/environment.go +++ b/internal/config/environment.go @@ -101,7 +101,10 @@ func (ib *InterpolatedBool) UnmarshalYAML(value *yaml.Node) error { return nil } -type InterpolatedMap map[string]interface{} +type InterpolatedMap struct { + Data map[string]interface{} + env map[string]string +} func (im *InterpolatedMap) UnmarshalYAML(value *yaml.Node) error { var data map[string]interface{} @@ -110,24 +113,50 @@ func (im *InterpolatedMap) UnmarshalYAML(value *yaml.Node) error { return errors.Wrapf(err, "could not decode value '%v' (line '%d') into map", value.Value, value.Line) } - for key, value := range data { - strVal, ok := value.(string) - if !ok { - continue - } - - if match := reVar.FindStringSubmatch(strVal); len(match) > 0 { - strVal = os.Getenv(match[1]) - } - - data[key] = strVal - } - - *im = data + im.Data = im.interpolateRecursive(data).(map[string]any) return nil } +func (im *InterpolatedMap) interpolateRecursive(data any) any { + if im.env == nil { + im.env = osEnvironment() + } + + switch typ := data.(type) { + case map[string]any: + for key, value := range typ { + typ[key] = im.interpolateRecursive(value) + } + + case string: + if match := reVar.FindStringSubmatch(typ); len(match) > 0 { + value, exists := im.env[match[1]] + if !exists { + data = "" + } + + data = value + } + + case []any: + for idx := range typ { + typ[idx] = im.interpolateRecursive(typ[idx]) + } + } + + return data +} + +func osEnvironment() map[string]string { + environ := os.Environ() + env := make(map[string]string, len(environ)) + for _, key := range environ { + env[key] = os.Getenv(key) + } + return env +} + type InterpolatedStringSlice []string func (iss *InterpolatedStringSlice) UnmarshalYAML(value *yaml.Node) error { diff --git a/internal/config/environment_test.go b/internal/config/environment_test.go new file mode 100644 index 0000000..74210b7 --- /dev/null +++ b/internal/config/environment_test.go @@ -0,0 +1,69 @@ +package config + +import ( + "fmt" + "os" + "testing" + + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +func TestInterpolatedMap(t *testing.T) { + type testCase struct { + Path string + Env map[string]string + Assert func(t *testing.T, parsed InterpolatedMap) + } + + testCases := []testCase{ + { + Path: "testdata/environment/interpolated-map-1.yml", + Env: map[string]string{ + "TEST_PROP1": "foo", + "TEST_SUB_PROP1": "bar", + "TEST_SUB2_PROP1": "baz", + }, + Assert: func(t *testing.T, parsed InterpolatedMap) { + if e, g := "foo", parsed.Data["prop1"]; e != g { + t.Errorf("parsed.Data[\"prop1\"]: expected '%v', got '%v'", e, g) + } + + if e, g := "bar", parsed.Data["sub"].(map[string]any)["subProp1"]; e != g { + t.Errorf("parsed.Data[\"sub\"][\"subProp1\"]: expected '%v', got '%v'", e, g) + } + + if e, g := "baz", parsed.Data["sub2"].(map[string]any)["sub2Prop1"].([]any)[0]; e != g { + t.Errorf("parsed.Data[\"sub2\"][\"sub2Prop1\"][0]: expected '%v', got '%v'", e, g) + } + + if e, g := "test", parsed.Data["sub2"].(map[string]any)["sub2Prop1"].([]any)[1]; e != g { + t.Errorf("parsed.Data[\"sub2\"][\"sub2Prop1\"][1]: expected '%v', got '%v'", e, g) + } + }, + }, + } + + for idx, tc := range testCases { + t.Run(fmt.Sprintf("Case #%d", idx), func(t *testing.T) { + data, err := os.ReadFile(tc.Path) + if err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + + var interpolatedMap InterpolatedMap + + if tc.Env != nil { + interpolatedMap.env = tc.Env + } + + if err := yaml.Unmarshal(data, &interpolatedMap); err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + + if tc.Assert != nil { + tc.Assert(t, interpolatedMap) + } + }) + } +} diff --git a/internal/config/metrics.go b/internal/config/metrics.go index 18f86ed..d08424c 100644 --- a/internal/config/metrics.go +++ b/internal/config/metrics.go @@ -17,9 +17,9 @@ func (c *BasicAuthConfig) CredentialsMap() map[string]string { return map[string]string{} } - credentials := make(map[string]string, len(*c.Credentials)) + credentials := make(map[string]string, len(c.Credentials.Data)) - for k, v := range *c.Credentials { + for k, v := range c.Credentials.Data { credentials[k] = fmt.Sprintf("%v", v) } diff --git a/internal/config/testdata/environment/interpolated-map-1.yml b/internal/config/testdata/environment/interpolated-map-1.yml new file mode 100644 index 0000000..b2d344a --- /dev/null +++ b/internal/config/testdata/environment/interpolated-map-1.yml @@ -0,0 +1,6 @@ +prop1: "${TEST_PROP1}" +prop2: 1 +sub: + subProp1: "${TEST_SUB_PROP1}" +sub2: + sub2Prop1: ["${TEST_SUB2_PROP1}", "test"]