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 3635d0b14..5e63588a9 100644 --- a/template/funcs.go +++ b/template/funcs.go @@ -673,6 +673,83 @@ 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, "/") + + 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 + } +} + +// 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") + } + + 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),