feat(config): interpolate recursively in interpolated map
Cadoles/bouncer/pipeline/head This commit looks good Details

This commit is contained in:
wpetit 2024-05-24 12:31:09 +02:00
parent 544326a4b7
commit 82c93d3f1e
6 changed files with 140 additions and 18 deletions

View File

@ -65,7 +65,7 @@ func (s *Server) bootstrapProxies(ctx context.Context) error {
for layerName, layerConfig := range proxyConfig.Layers { for layerName, layerConfig := range proxyConfig.Layers {
layerType := store.LayerType(layerConfig.Type) 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 { if _, err := layerRepo.CreateLayer(ctx, proxyName, layerName, layerType, layerOptions); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@ -109,7 +109,7 @@ func (s *Server) validateBootstrap(ctx context.Context) error {
} }
rawOptions := func(opts config.InterpolatedMap) map[string]any { rawOptions := func(opts config.InterpolatedMap) map[string]any {
return opts return opts.Data
}(layerConf.Options) }(layerConf.Options)
if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil { if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil {

View File

@ -101,33 +101,66 @@ func (ib *InterpolatedBool) UnmarshalYAML(value *yaml.Node) error {
return nil 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 { 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 { if err := value.Decode(&data); err != nil {
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into map", value.Value, value.Line) return errors.Wrapf(err, "could not decode value '%v' (line '%d') into map", value.Value, value.Line)
} }
for key, value := range data { if im.getEnv == nil {
strVal, ok := value.(string) im.getEnv = os.Getenv
if !ok {
continue
} }
if match := reVar.FindStringSubmatch(strVal); len(match) > 0 { interpolated, err := im.interpolateRecursive(data)
strVal = os.Getenv(match[1]) if err != nil {
return errors.WithStack(err)
} }
data[key] = strVal im.Data = interpolated.(map[string]any)
}
*im = data
return nil 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 type InterpolatedStringSlice []string
func (iss *InterpolatedStringSlice) UnmarshalYAML(value *yaml.Node) error { func (iss *InterpolatedStringSlice) UnmarshalYAML(value *yaml.Node) error {

View File

@ -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)
}
})
}
}

View File

@ -17,9 +17,9 @@ func (c *BasicAuthConfig) CredentialsMap() map[string]string {
return 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) credentials[k] = fmt.Sprintf("%v", v)
} }

View File

@ -0,0 +1,6 @@
prop1: "${TEST_PROP1}"
prop2: 1
sub:
subProp1: "${TEST_SUB_PROP1}"
sub2:
sub2Prop1: ["${TEST_SUB2_PROP1}", "test"]

View File

@ -0,0 +1 @@
foo: http://${BAR}