Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 54 additions & 4 deletions pkg/util/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@ import (
"net/url"
"os"
"path/filepath"
"reflect"
"strings"
"text/template"

"github.com/Masterminds/sprig/v3"
"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.
Expand Down Expand Up @@ -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() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like Sprig made the opinionated choice to use map[string]interface{} only https://github.com/Masterminds/sprig/blob/39e4d5d0e0d566a256746e59749821155f209d11/dict.go#L8

Do you know if we may ever run into cases where a struct is passed to this function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map only is fine. I was trying to be super-flexible but probably unnecessary :-).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm under the impression that we may have to check reflect.Value.IsNil before we dereference the pointer, otherwise the code could panic.

Copy link
Contributor Author

@yolken-segment yolken-segment Feb 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a check here.

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 {
Expand Down
85 changes: 85 additions & 0 deletions pkg/util/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
2 changes: 1 addition & 1 deletion pkg/version/version.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package version

// Version stores the current kubeapply version.
const Version = "0.0.27"
const Version = "0.0.28"