diff --git a/pkg/util/template.go b/pkg/util/template.go index 68c4656..e9958c5 100644 --- a/pkg/util/template.go +++ b/pkg/util/template.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "path/filepath" + "reflect" "strings" "text/template" @@ -15,10 +16,14 @@ import ( "github.com/ghodss/yaml" ) -var extraTemplateFuncs = template.FuncMap{ - "urlEncode": url.QueryEscape, - "toYaml": toYaml, -} +var ( + extraTemplateFuncs = template.FuncMap{ + "lookup": lookup, + "pathLookup": pathLookup, + "toYaml": toYaml, + "urlEncode": url.QueryEscape, + } +) // ApplyTemplate runs golang templating on all files in the provided path, // replacing them in-place with their templated versions. @@ -214,6 +219,51 @@ func configMapEntriesGenerator( } } +// lookup does a dot-separated path lookup on the input map. If a key on the path is +// not found, it returns nil. If the input or any of its children on the lookup path is not +// a map, it returns an error. +func lookup(input interface{}, path string) (interface{}, error) { + obj := reflect.ValueOf(input) + components := strings.Split(path, ".") + + for i := 0; i < len(components); { + switch obj.Kind() { + case reflect.Map: + obj = obj.MapIndex(reflect.ValueOf(components[i])) + i++ + case reflect.Ptr, reflect.Interface: + if obj.IsNil() { + return nil, nil + } + + // Get the thing being pointed to or interfaced, don't advance index + obj = obj.Elem() + default: + if obj.IsValid() { + // An object was found, but it's not a map. Return an error. + return nil, fmt.Errorf( + "Tried to traverse a value that's not a map (kind=%s)", + obj.Kind(), + ) + } + + // An intermediate key wasn't found + return nil, nil + } + } + + if !obj.IsValid() { + // The last key wasn't found + return nil, nil + } + return obj.Interface(), nil +} + +// pathLookup is the same as lookup, but with the arguments flipped. +func pathLookup(path string, input interface{}) (interface{}, error) { + return lookup(input, path) +} + func toYaml(input interface{}) (string, error) { bytes, err := yaml.Marshal(input) if err != nil { diff --git a/pkg/util/template_test.go b/pkg/util/template_test.go index 6ef50d0..302055a 100644 --- a/pkg/util/template_test.go +++ b/pkg/util/template_test.go @@ -136,3 +136,88 @@ func fileContents(t *testing.T, path string) string { return string(contents) } + +func TestLookup(t *testing.T) { + m := map[string]interface{}{ + "key1": "value1", + "key2": map[string]interface{}{ + "key3": map[string]interface{}{ + "key4": "value4", + }, + "key5": 1234, + }, + "key6": nil, + } + + type testCase struct { + input interface{} + path string + expectedResult interface{} + expectErr bool + } + + testCases := []testCase{ + { + input: m, + path: "bad-key", + expectedResult: nil, + }, + { + input: m, + path: "", + expectedResult: nil, + }, + { + input: nil, + path: "key1", + expectedResult: nil, + }, + { + input: "not a map", + path: "key1", + expectedResult: nil, + expectErr: true, + }, + { + input: m, + path: "key1", + expectedResult: "value1", + }, + { + input: &m, + path: "key1", + expectedResult: "value1", + }, + { + input: m, + path: "key1.not-a-map", + expectedResult: nil, + expectErr: true, + }, + { + input: m, + path: "key2.key3.key4", + expectedResult: "value4", + }, + { + input: m, + path: "key2.key5", + expectedResult: 1234, + }, + { + input: m, + path: "key6.nil-key", + expectedResult: nil, + }, + } + + for index, tc := range testCases { + result, err := lookup(tc.input, tc.path) + assert.Equal(t, tc.expectedResult, result, "Unexpected result for case %d", index) + if tc.expectErr { + assert.Error(t, err, "Did not get expected error in case %d", index) + } else { + assert.NoError(t, err, "Got unexpected error in case %d", index) + } + } +} diff --git a/pkg/version/version.go b/pkg/version/version.go index ea85fdf..af9a452 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,4 +1,4 @@ package version // Version stores the current kubeapply version. -const Version = "0.0.27" +const Version = "0.0.28"