2
Copyright The Helm Authors.
4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
8
http://www.apache.org/licenses/LICENSE-2.0
10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
29
"github.com/pkg/errors"
31
"helm.sh/helm/v3/pkg/chart"
32
"helm.sh/helm/v3/pkg/chartutil"
35
var Disks map[string]interface{} = map[string]interface{}{}
36
var LookupFunc func(resource string, namespace string, name string) (map[string]interface{}, error) = func(string, string, string) (map[string]interface{}, error) {
37
return map[string]interface{}{}, nil
40
// Engine is an implementation of the Helm rendering implementation for templates.
42
// If strict is enabled, template rendering will fail if a template references
43
// a value that was not passed in.
45
// In LintMode, some 'required' template values may be missing, so don't fail
47
// EnableDNS tells the engine to allow DNS lookups when rendering templates
51
// Render takes a chart, optional values, and value overrides, and attempts to render the Go templates.
53
// Render can be called repeatedly on the same engine.
55
// This will look in the chart's 'templates' data (e.g. the 'templates/' directory)
56
// and attempt to render the templates there using the values passed in.
58
// Values are scoped to their templates. A dependency template will not have
59
// access to the values set for its parent. If chart "foo" includes chart "bar",
60
// "bar" will not have access to the values for "foo".
62
// Values should be prepared with something like `chartutils.ReadValues`.
64
// Values are passed through the templates according to scope. If the top layer
65
// chart includes the chart foo, which includes the chart bar, the values map
66
// will be examined for a table called "foo". If "foo" is found in vals,
67
// that section of the values will be passed into the "foo" chart. And if that
68
// section contains a value named "bar", that value will be passed on to the
69
// bar chart during render time.
70
func (e Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
71
tmap := allTemplates(chrt, values)
75
// Render takes a chart, optional values, and value overrides, and attempts to
76
// render the Go templates using the default options.
77
func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
78
return new(Engine).Render(chrt, values)
81
// renderable is an object that can be rendered.
82
type renderable struct {
83
// tpl is the current template.
85
// vals are the values to be supplied to the template.
87
// namespace prefix to the templates of the current chart
91
const warnStartDelim = "HELM_ERR_START"
92
const warnEndDelim = "HELM_ERR_END"
93
const recursionMaxNums = 1000
95
var warnRegex = regexp.MustCompile(warnStartDelim + `((?s).*)` + warnEndDelim)
97
func warnWrap(warn string) string {
98
return warnStartDelim + warn + warnEndDelim
101
// 'include' needs to be defined in the scope of a 'tpl' template as
102
// well as regular file-loaded templates.
103
func includeFun(t *template.Template, includedNames map[string]int) func(string, interface{}) (string, error) {
104
return func(name string, data interface{}) (string, error) {
105
var buf strings.Builder
106
if v, ok := includedNames[name]; ok {
107
if v > recursionMaxNums {
108
return "", errors.Wrapf(fmt.Errorf("unable to execute template"), "rendering template has a nested reference name: %s", name)
110
includedNames[name]++
112
includedNames[name] = 1
114
err := t.ExecuteTemplate(&buf, name, data)
115
includedNames[name]--
116
return buf.String(), err
120
// As does 'tpl', so that nested calls to 'tpl' see the templates
121
// defined by their enclosing contexts.
122
func tplFun(parent *template.Template, includedNames map[string]int, strict bool) func(string, interface{}) (string, error) {
123
return func(tpl string, vals interface{}) (string, error) {
124
t, err := parent.Clone()
126
return "", errors.Wrapf(err, "cannot clone template")
129
// Re-inject the missingkey option, see text/template issue https://github.com/golang/go/issues/43022
130
// We have to go by strict from our engine configuration, as the option fields are private in Template.
131
// TODO: Remove workaround (and the strict parameter) once we build only with golang versions with a fix.
133
t.Option("missingkey=error")
135
t.Option("missingkey=zero")
138
// Re-inject 'include' so that it can close over our clone of t;
139
// this lets any 'define's inside tpl be 'include'd.
140
t.Funcs(template.FuncMap{
141
"include": includeFun(t, includedNames),
142
"tpl": tplFun(t, includedNames, strict),
145
// We need a .New template, as template text which is just blanks
146
// or comments after parsing out defines just addes new named
147
// template definitions without changing the main template.
148
// https://pkg.go.dev/text/template#Template.Parse
149
// Use the parent's name for lack of a better way to identify the tpl
150
// text string. (Maybe we could use a hash appended to the name?)
151
t, err = t.New(parent.Name()).Parse(tpl)
153
return "", errors.Wrapf(err, "cannot parse template %q", tpl)
156
var buf strings.Builder
157
if err := t.Execute(&buf, vals); err != nil {
158
return "", errors.Wrapf(err, "error during tpl function execution for %q", tpl)
161
// See comment in renderWithReferences explaining the <no value> hack.
162
return strings.ReplaceAll(buf.String(), "<no value>", ""), nil
166
// initFunMap creates the Engine's FuncMap and adds context-specific functions.
167
func (e Engine) initFunMap(t *template.Template) {
169
includedNames := make(map[string]int)
171
// Add the template-rendering functions here so we can close over t.
172
funcMap["include"] = includeFun(t, includedNames)
173
funcMap["tpl"] = tplFun(t, includedNames, e.Strict)
175
// Add the `required` function here so we can use lintMode
176
funcMap["required"] = func(warn string, val interface{}) (interface{}, error) {
179
// Don't fail on missing required values when linting
180
log.Printf("[INFO] Missing required value: %s", warn)
183
return val, errors.Errorf(warnWrap(warn))
184
} else if _, ok := val.(string); ok {
187
// Don't fail on missing required values when linting
188
log.Printf("[INFO] Missing required value: %s", warn)
191
return val, errors.Errorf(warnWrap(warn))
197
// Override sprig fail function for linting and wrapping message
198
funcMap["fail"] = func(msg string) (string, error) {
200
// Don't fail when linting
201
log.Printf("[INFO] Fail: %s", msg)
204
return "", errors.New(warnWrap(msg))
207
// If we are not linting and have a cluster connection, provide a Kubernetes-backed
210
funcMap["lookup"] = LookupFunc
213
// When DNS lookups are not enabled override the sprig function and return
216
funcMap["getHostByName"] = func(name string) string {
224
// render takes a map of templates/values and renders them.
225
func (e Engine) render(tpls map[string]renderable) (rendered map[string]string, err error) {
226
// Basically, what we do here is start with an empty parent template and then
227
// build up a list of templates -- one for each file. Once all of the templates
228
// have been parsed, we loop through again and execute every template.
230
// The idea with this process is to make it possible for more complex templates
231
// to share common blocks, but to make the entire thing feel like a file-based
234
if r := recover(); r != nil {
235
err = errors.Errorf("rendering template failed: %v", r)
238
t := template.New("gotpl")
240
t.Option("missingkey=error")
242
// Not that zero will attempt to add default values for types it knows,
243
// but will still emit <no value> for others. We mitigate that later.
244
t.Option("missingkey=zero")
249
// We want to parse the templates in a predictable order. The order favors
250
// higher-level (in file system) templates over deeply nested templates.
251
keys := sortTemplates(tpls)
253
for _, filename := range keys {
255
if _, err := t.New(filename).Parse(r.tpl); err != nil {
256
return map[string]string{}, cleanupParseError(filename, err)
260
rendered = make(map[string]string, len(keys))
261
for _, filename := range keys {
262
// Don't render partials. We don't care out the direct output of partials.
263
// They are only included from other templates.
264
if strings.HasPrefix(path.Base(filename), "_") {
267
// At render time, add information about the template that is being rendered.
268
vals := tpls[filename].vals
269
vals["Template"] = chartutil.Values{"Name": filename, "BasePath": tpls[filename].basePath}
270
var buf strings.Builder
271
if err := t.ExecuteTemplate(&buf, filename, vals); err != nil {
272
return map[string]string{}, cleanupExecError(filename, err)
275
// Work around the issue where Go will emit "<no value>" even if Options(missing=zero)
276
// is set. Since missing=error will never get here, we do not need to handle
278
rendered[filename] = strings.ReplaceAll(buf.String(), "<no value>", "")
284
func cleanupParseError(filename string, err error) error {
285
tokens := strings.Split(err.Error(), ": ")
286
if len(tokens) == 1 {
287
// This might happen if a non-templating error occurs
288
return fmt.Errorf("parse error in (%s): %s", filename, err)
290
// The first token is "template"
291
// The second token is either "filename:lineno" or "filename:lineNo:columnNo"
292
location := tokens[1]
293
// The remaining tokens make up a stacktrace-like chain, ending with the relevant error
294
errMsg := tokens[len(tokens)-1]
295
return fmt.Errorf("parse error at (%s): %s", string(location), errMsg)
298
func cleanupExecError(filename string, err error) error {
299
if _, isExecError := err.(template.ExecError); !isExecError {
303
tokens := strings.SplitN(err.Error(), ": ", 3)
304
if len(tokens) != 3 {
305
// This might happen if a non-templating error occurs
306
return fmt.Errorf("execution error in (%s): %s", filename, err)
309
// The first token is "template"
310
// The second token is either "filename:lineno" or "filename:lineNo:columnNo"
311
location := tokens[1]
313
parts := warnRegex.FindStringSubmatch(tokens[2])
315
return fmt.Errorf("execution error at (%s): %s", string(location), parts[1])
321
func sortTemplates(tpls map[string]renderable) []string {
322
keys := make([]string, len(tpls))
324
for key := range tpls {
328
sort.Sort(sort.Reverse(byPathLen(keys)))
332
type byPathLen []string
334
func (p byPathLen) Len() int { return len(p) }
335
func (p byPathLen) Swap(i, j int) { p[j], p[i] = p[i], p[j] }
336
func (p byPathLen) Less(i, j int) bool {
338
ca, cb := strings.Count(a, "/"), strings.Count(b, "/")
340
return strings.Compare(a, b) == -1
345
// allTemplates returns all templates for a chart and its dependencies.
347
// As it goes, it also prepares the values in a scope-sensitive manner.
348
func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
349
templates := make(map[string]renderable)
350
recAllTpls(c, templates, vals)
354
// recAllTpls recurses through the templates in a chart.
356
// As it recurses, it also sets the values to be appropriate for the template
358
func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) map[string]interface{} {
359
subCharts := make(map[string]interface{})
360
chartMetaData := struct {
363
}{*c.Metadata, c.IsRoot()}
365
next := map[string]interface{}{
366
"Chart": chartMetaData,
367
"Files": newFiles(c.Files),
368
"Release": vals["Release"],
369
"Capabilities": vals["Capabilities"],
370
"Values": make(chartutil.Values),
371
"Subcharts": subCharts,
375
// If there is a {{.Values.ThisChart}} in the parent metadata,
376
// copy that into the {{.Values}} for this template.
378
next["Values"] = vals["Values"]
379
} else if vs, err := vals.Table("Values." + c.Name()); err == nil {
383
for _, child := range c.Dependencies() {
384
subCharts[child.Name()] = recAllTpls(child, templates, next)
387
newParentID := c.ChartFullPath()
388
for _, t := range c.Templates {
392
if !isTemplateValid(c, t.Name) {
395
templates[path.Join(newParentID, t.Name)] = renderable{
398
basePath: path.Join(newParentID, "templates"),
405
// isTemplateValid returns true if the template is valid for the chart type
406
func isTemplateValid(ch *chart.Chart, templateName string) bool {
407
if isLibraryChart(ch) {
408
return strings.HasPrefix(filepath.Base(templateName), "_")
413
// isLibraryChart returns true if the chart is a library chart
414
func isLibraryChart(c *chart.Chart) bool {
415
return strings.EqualFold(c.Metadata.Type, "library")