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..437b78f 100644 --- a/internal/config/environment.go +++ b/internal/config/environment.go @@ -101,33 +101,66 @@ func (ib *InterpolatedBool) UnmarshalYAML(value *yaml.Node) error { return nil } -type InterpolatedMap map[string]interface{} +type InterpolatedMap struct { + Data map[string]any + getEnv func(string) string +} func (im *InterpolatedMap) UnmarshalYAML(value *yaml.Node) error { - var data map[string]interface{} + var data map[string]any if err := value.Decode(&data); err != nil { 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 + if im.getEnv == nil { + im.getEnv = os.Getenv } - *im = data + interpolated, err := im.interpolateRecursive(data) + if err != nil { + return errors.WithStack(err) + } + + im.Data = interpolated.(map[string]any) return nil } +func (im *InterpolatedMap) interpolateRecursive(data any) (any, error) { + switch typ := data.(type) { + case map[string]any: + for key, value := range typ { + value, err := im.interpolateRecursive(value) + if err != nil { + return nil, errors.WithStack(err) + } + + typ[key] = value + } + + case string: + value, err := envsubst.Eval(typ, im.getEnv) + if err != nil { + return nil, errors.WithStack(err) + } + + data = value + + case []any: + for idx := range typ { + value, err := im.interpolateRecursive(typ[idx]) + if err != nil { + return nil, errors.WithStack(err) + } + + typ[idx] = value + } + } + + return data, nil +} + 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..6044667 --- /dev/null +++ b/internal/config/environment_test.go @@ -0,0 +1,82 @@ +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) + } + }, + }, + { + Path: "testdata/environment/interpolated-map-2.yml", + Env: map[string]string{ + "BAR": "bar", + }, + Assert: func(t *testing.T, parsed InterpolatedMap) { + if e, g := "http://bar", parsed.Data["foo"]; e != g { + t.Errorf("parsed.Data[\"foo\"]: 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.getEnv = func(key string) string { + return tc.Env[key] + } + } + + 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"] diff --git a/internal/config/testdata/environment/interpolated-map-2.yml b/internal/config/testdata/environment/interpolated-map-2.yml new file mode 100644 index 0000000..4dfdf0a --- /dev/null +++ b/internal/config/testdata/environment/interpolated-map-2.yml @@ -0,0 +1 @@ +foo: http://${BAR}