diff --git a/pkg/api/core/template/functions.go b/pkg/api/core/template/functions.go new file mode 100644 index 00000000..22fd0818 --- /dev/null +++ b/pkg/api/core/template/functions.go @@ -0,0 +1,186 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package template + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + "text/template" + + "github.com/Masterminds/sprig/v3" + "github.com/pelletier/go-toml" + "github.com/pkg/errors" + "gopkg.in/yaml.v2" +) + +const recursionMaxNums = 1000 + +// funcMap returns a mapping of all of the functions that Engine has. +// +// Because some functions are late-bound (e.g. contain context-sensitive +// data), the functions may not all perform identically outside of an Engine +// as they will inside of an Engine. +// +// Known late-bound functions: +// +// - "include" +// - "tpl" +// +// These are late-bound in Engine.Render(). The +// version included in the FuncMap is a placeholder. +// +func funcMap() template.FuncMap { + f := sprig.TxtFuncMap() + + // Add some extra functionality + extra := template.FuncMap{ + "toToml": toTOML, + "toYaml": toYAML, + "fromYaml": fromYAML, + "fromYamlArray": fromYAMLArray, + "toJson": toJSON, + "fromJson": fromJSON, + "fromJsonArray": fromJSONArray, + + // This is a placeholder for the "include" function, which is + // late-bound to a template. By declaring it here, we preserve the + // integrity of the linter. + "include": func(string, interface{}) string { return "not implemented" }, + } + + for k, v := range extra { + f[k] = v + } + + return f +} + +// toYAML takes an interface, marshals it to yaml, and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toYAML(v interface{}) string { + data, err := yaml.Marshal(v) + if err != nil { + // Swallow errors inside of a template. + return "" + } + return strings.TrimSuffix(string(data), "\n") +} + +// fromYAML converts a YAML document into a map[string]interface{}. +// +// This is not a general-purpose YAML parser, and will not parse all valid +// YAML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string into +// m["Error"] in the returned map. +func fromYAML(str string) map[string]interface{} { + m := map[string]interface{}{} + + if err := yaml.Unmarshal([]byte(str), &m); err != nil { + m["Error"] = err.Error() + } + return m +} + +// fromYAMLArray converts a YAML array into a []interface{}. +// +// This is not a general-purpose YAML parser, and will not parse all valid +// YAML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string as +// the first and only item in the returned array. +func fromYAMLArray(str string) []interface{} { + a := []interface{}{} + + if err := yaml.Unmarshal([]byte(str), &a); err != nil { + a = []interface{}{err.Error()} + } + return a +} + +// toTOML takes an interface, marshals it to toml, and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toTOML(v interface{}) string { + b := bytes.NewBuffer(nil) + e := toml.NewEncoder(b) + err := e.Encode(v) + if err != nil { + return err.Error() + } + return b.String() +} + +// toJSON takes an interface, marshals it to json, and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toJSON(v interface{}) string { + data, err := json.Marshal(v) + if err != nil { + // Swallow errors inside of a template. + return "" + } + return string(data) +} + +// fromJSON converts a JSON document into a map[string]interface{}. +// +// This is not a general-purpose JSON parser, and will not parse all valid +// JSON documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string into +// m["Error"] in the returned map. +func fromJSON(str string) map[string]interface{} { + m := make(map[string]interface{}) + + if err := json.Unmarshal([]byte(str), &m); err != nil { + m["Error"] = err.Error() + } + return m +} + +// fromJSONArray converts a JSON array into a []interface{}. +// +// This is not a general-purpose JSON parser, and will not parse all valid +// JSON documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string as +// the first and only item in the returned array. +func fromJSONArray(str string) []interface{} { + a := []interface{}{} + + if err := json.Unmarshal([]byte(str), &a); err != nil { + a = []interface{}{err.Error()} + } + return a +} + +func includeTemplate(tmpl *template.Template, includedNames map[string]int) func(name string, data interface{}) (string, error) { + return func(name string, data interface{}) (string, error) { + var buf strings.Builder + if v, ok := includedNames[name]; ok { + if v > recursionMaxNums { + return "", errors.Wrapf(fmt.Errorf("unable to execute template"), "rendering template has a nested reference name: %s", name) + } + includedNames[name]++ + } else { + includedNames[name] = 1 + } + err := tmpl.ExecuteTemplate(&buf, name, data) + includedNames[name]-- + return buf.String(), err + } +} diff --git a/pkg/api/core/template/string.go b/pkg/api/core/template/string.go index 92dfcaa8..623c8432 100644 --- a/pkg/api/core/template/string.go +++ b/pkg/api/core/template/string.go @@ -26,14 +26,23 @@ import ( "github.com/pkg/errors" "gopkg.in/yaml.v2" - "github.com/Masterminds/sprig/v3" "github.com/imdario/mergo" ) // String templates a string with the interface func String(t string, i interface{}) (string, error) { b := bytes.NewBuffer([]byte{}) - tmpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(t) + + f := funcMap() + + tmpl := template.New("") + + includedNames := make(map[string]int) + + // Add the 'include' function here so we can close over tmpl. + f["include"] = includeTemplate(tmpl, includedNames) + + tmpl, err := tmpl.Funcs(f).Parse(t) if err != nil { return "", err } diff --git a/pkg/api/core/template/string_test.go b/pkg/api/core/template/string_test.go index 14544b33..fd54f451 100644 --- a/pkg/api/core/template/string_test.go +++ b/pkg/api/core/template/string_test.go @@ -190,5 +190,33 @@ faa: "baz" Expect(res).To(Equal("")) }) + + It("correctly parses `include`", func() { + testDir, err := ioutil.TempDir(os.TempDir(), "test") + Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(testDir) + + toTemplate := filepath.Join(testDir, "totemplate.yaml") + values := filepath.Join(testDir, "values.yaml") + d := filepath.Join(testDir, "default.yaml") + + writeFile(toTemplate, ` +{{- define "app" -}} +app_name: {{if .Values.foo}}{{.Values.foo}}{{end}} +{{- end -}} +{{ include "app" . | indent 4 }} +`) + writeFile(values, ` +foo: "bar" +`) + writeFile(d, ``) + + Expect(err).ToNot(HaveOccurred()) + + res, err := RenderWithValues([]string{toTemplate}, values, d) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal(` app_name: bar +`)) + }) }) })