From e37a508d09cd83cde39d5bff1ddc44d8fbef4c33 Mon Sep 17 00:00:00 2001 From: Alexey Kamenskiy Date: Thu, 23 Nov 2023 15:40:07 +1100 Subject: [PATCH 1/2] Add treeYAML helper method --- template/funcs.go | 75 ++++++++++++++++++++++++++++++++++++++++++++ template/template.go | 1 + 2 files changed, 76 insertions(+) diff --git a/template/funcs.go b/template/funcs.go index 3635d0b14..3d0ef8d8d 100644 --- a/template/funcs.go +++ b/template/funcs.go @@ -673,6 +673,81 @@ func treeFunc(b *Brain, used, missing *dep.Set, emptyIsSafe bool) func(string) ( } } +func treeYAML(b *Brain, used, missing *dep.Set, emptyIsSafe bool) func(string) (map[interface{}]interface{}, error) { + return func(s string) (map[interface{}]interface{}, error) { + s = strings.Trim(s, "/") + + result := make(map[interface{}]interface{}) + + d, err := dep.NewKVListQuery(s) + if err != nil { + return result, err + } + + used.Add(d) + + // Only return non-empty top-level keys + if value, ok := b.Recall(d); ok { + for _, pair := range value.([]*dep.KeyPair) { + parts := strings.Split(pair.Key, "/") + if parts[len(parts)-1] != "" { + //result = append(result, pair) + var v interface{} + if err := yaml.Unmarshal([]byte(pair.Value), &v); err != nil { + return nil, errors.Wrap(err, "treeYAML") + } + + result, err = mapInjectHelper(result, parts, v) + if err != nil { + return nil, err + } + } + } + + if len(result) == 0 { + if emptyIsSafe { + // Operator used potentially unsafe tree function in the template instead of the safeTree + return result, nil + } + } else { + // non empty result is good so we just return the data + return result, nil + } + + // If we reach this part of the code result is completely empty as value returned no KV pairs + // Operator selected to use safeTree on the specific KV prefix so we will refuse to render template + // by marking d as missing + } + + // b.Recall either returned an error or safeTree entered unsafe case + missing.Add(d) + + return result, nil + } +} + +func mapInjectHelper(dst map[interface{}]interface{}, path []string, v interface{}) (map[interface{}]interface{}, error) { + if len(path) == 0 { + return nil, fmt.Errorf("ran out of key parts before finished injection into map") + } + + childNode, ok := dst[path[0]] + if !ok { + if len(path) > 1 { + var err error + childNode, err = mapInjectHelper(make(map[interface{}]interface{}), path[1:], v) + if err != nil { + return nil, err + } + } else { + childNode = v + } + dst[path[0]] = childNode + } + + return dst, nil +} + // base64Decode decodes the given string as a base64 string, returning an error // if it fails. func base64Decode(s string) (string, error) { diff --git a/template/template.go b/template/template.go index f43a62dd6..fab09d6bd 100644 --- a/template/template.go +++ b/template/template.go @@ -334,6 +334,7 @@ func funcMap(i *funcMapInput) template.FuncMap { "connect": connectFunc(i.brain, i.used, i.missing), "services": servicesFunc(i.brain, i.used, i.missing), "tree": treeFunc(i.brain, i.used, i.missing, true), + "treeYAML": treeYAML(i.brain, i.used, i.missing, true), "safeTree": safeTreeFunc(i.brain, i.used, i.missing), "caRoots": connectCARootsFunc(i.brain, i.used, i.missing), "caLeaf": connectLeafFunc(i.brain, i.used, i.missing), From 39e7a6f7997bc2d33a4e6c12e4538221186b218d Mon Sep 17 00:00:00 2001 From: Alexey Kamenskiy Date: Fri, 24 Nov 2023 09:42:19 +1100 Subject: [PATCH 2/2] Add treeYAML helper method documentation --- docs/templating-language.md | 30 ++++++++++++++++++++++++++++++ template/funcs.go | 2 ++ 2 files changed, 32 insertions(+) diff --git a/docs/templating-language.md b/docs/templating-language.md index 6357b4a05..74057e352 100644 --- a/docs/templating-language.md +++ b/docs/templating-language.md @@ -809,6 +809,36 @@ nested/config/value "value" Unlike [`ls`](#ls), [`tree`](#tree) returns **all** keys under the prefix, just like the Unix [`tree`](#tree) command. +### `treeYAML` + +Similarly to [`tree`](#tree) query [Consul][consul] for all kv pairs at the given key path. It will interpret loaded kv pairs as YAML values inferring a type or parsing value as YAML structure and return data as a structure suitable for other YAML related methods processing + +```golang +{{ treeYAML "" }} +``` + +For example with following KV data in Consul: + +``` +config/applications/debug = false +config/applications/ports = "- 8080\n- 8480" +``` + +the template: + +```golang +{{ treeYAML "config/applications" | toYAML }} +``` + +will render: + +```yaml +debug: false +ports: + - 8080 + - 8480 +``` + ### `safeTree` Same as [`tree`](#tree), but refuse to render template, if the KV prefix query return blank/empty data. diff --git a/template/funcs.go b/template/funcs.go index 3d0ef8d8d..5e63588a9 100644 --- a/template/funcs.go +++ b/template/funcs.go @@ -673,6 +673,7 @@ func treeFunc(b *Brain, used, missing *dep.Set, emptyIsSafe bool) func(string) ( } } +// treeYAML returns tree as YAML data performing type inference or yaml unmarshalling of the data. func treeYAML(b *Brain, used, missing *dep.Set, emptyIsSafe bool) func(string) (map[interface{}]interface{}, error) { return func(s string) (map[interface{}]interface{}, error) { s = strings.Trim(s, "/") @@ -726,6 +727,7 @@ func treeYAML(b *Brain, used, missing *dep.Set, emptyIsSafe bool) func(string) ( } } +// mapInjectHelper takes a path of YAML leaf node, its value and recursively traverses destination YAML structure to inject the value. func mapInjectHelper(dst map[interface{}]interface{}, path []string, v interface{}) (map[interface{}]interface{}, error) { if len(path) == 0 { return nil, fmt.Errorf("ran out of key parts before finished injection into map")