1/*
2Copyright The Helm Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package engine
18
19import (
20	"fmt"
21	"log"
22	"path"
23	"path/filepath"
24	"regexp"
25	"sort"
26	"strings"
27	"text/template"
28
29	"github.com/pkg/errors"
30	"k8s.io/client-go/rest"
31
32	"helm.sh/helm/v3/pkg/chart"
33	"helm.sh/helm/v3/pkg/chartutil"
34)
35
36// Engine is an implementation of the Helm rendering implementation for templates.
37type Engine struct {
38	// If strict is enabled, template rendering will fail if a template references
39	// a value that was not passed in.
40	Strict bool
41	// In LintMode, some 'required' template values may be missing, so don't fail
42	LintMode bool
43	// the rest config to connect to the kubernetes api
44	config *rest.Config
45}
46
47// Render takes a chart, optional values, and value overrides, and attempts to render the Go templates.
48//
49// Render can be called repeatedly on the same engine.
50//
51// This will look in the chart's 'templates' data (e.g. the 'templates/' directory)
52// and attempt to render the templates there using the values passed in.
53//
54// Values are scoped to their templates. A dependency template will not have
55// access to the values set for its parent. If chart "foo" includes chart "bar",
56// "bar" will not have access to the values for "foo".
57//
58// Values should be prepared with something like `chartutils.ReadValues`.
59//
60// Values are passed through the templates according to scope. If the top layer
61// chart includes the chart foo, which includes the chart bar, the values map
62// will be examined for a table called "foo". If "foo" is found in vals,
63// that section of the values will be passed into the "foo" chart. And if that
64// section contains a value named "bar", that value will be passed on to the
65// bar chart during render time.
66func (e Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
67	tmap := allTemplates(chrt, values)
68	return e.render(tmap)
69}
70
71// Render takes a chart, optional values, and value overrides, and attempts to
72// render the Go templates using the default options.
73func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
74	return new(Engine).Render(chrt, values)
75}
76
77// RenderWithClient takes a chart, optional values, and value overrides, and attempts to
78// render the Go templates using the default options. This engine is client aware and so can have template
79// functions that interact with the client
80func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config) (map[string]string, error) {
81	return Engine{
82		config: config,
83	}.Render(chrt, values)
84}
85
86// renderable is an object that can be rendered.
87type renderable struct {
88	// tpl is the current template.
89	tpl string
90	// vals are the values to be supplied to the template.
91	vals chartutil.Values
92	// namespace prefix to the templates of the current chart
93	basePath string
94}
95
96const warnStartDelim = "HELM_ERR_START"
97const warnEndDelim = "HELM_ERR_END"
98const recursionMaxNums = 1000
99
100var warnRegex = regexp.MustCompile(warnStartDelim + `(.*)` + warnEndDelim)
101
102func warnWrap(warn string) string {
103	return warnStartDelim + warn + warnEndDelim
104}
105
106// initFunMap creates the Engine's FuncMap and adds context-specific functions.
107func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]renderable) {
108	funcMap := funcMap()
109	includedNames := make(map[string]int)
110
111	// Add the 'include' function here so we can close over t.
112	funcMap["include"] = func(name string, data interface{}) (string, error) {
113		var buf strings.Builder
114		if v, ok := includedNames[name]; ok {
115			if v > recursionMaxNums {
116				return "", errors.Wrapf(fmt.Errorf("unable to execute template"), "rendering template has a nested reference name: %s", name)
117			}
118			includedNames[name]++
119		} else {
120			includedNames[name] = 1
121		}
122		err := t.ExecuteTemplate(&buf, name, data)
123		includedNames[name]--
124		return buf.String(), err
125	}
126
127	// Add the 'tpl' function here
128	funcMap["tpl"] = func(tpl string, vals chartutil.Values) (string, error) {
129		basePath, err := vals.PathValue("Template.BasePath")
130		if err != nil {
131			return "", errors.Wrapf(err, "cannot retrieve Template.Basepath from values inside tpl function: %s", tpl)
132		}
133
134		templateName, err := vals.PathValue("Template.Name")
135		if err != nil {
136			return "", errors.Wrapf(err, "cannot retrieve Template.Name from values inside tpl function: %s", tpl)
137		}
138
139		templates := map[string]renderable{
140			templateName.(string): {
141				tpl:      tpl,
142				vals:     vals,
143				basePath: basePath.(string),
144			},
145		}
146
147		result, err := e.renderWithReferences(templates, referenceTpls)
148		if err != nil {
149			return "", errors.Wrapf(err, "error during tpl function execution for %q", tpl)
150		}
151		return result[templateName.(string)], nil
152	}
153
154	// Add the `required` function here so we can use lintMode
155	funcMap["required"] = func(warn string, val interface{}) (interface{}, error) {
156		if val == nil {
157			if e.LintMode {
158				// Don't fail on missing required values when linting
159				log.Printf("[INFO] Missing required value: %s", warn)
160				return "", nil
161			}
162			return val, errors.Errorf(warnWrap(warn))
163		} else if _, ok := val.(string); ok {
164			if val == "" {
165				if e.LintMode {
166					// Don't fail on missing required values when linting
167					log.Printf("[INFO] Missing required value: %s", warn)
168					return "", nil
169				}
170				return val, errors.Errorf(warnWrap(warn))
171			}
172		}
173		return val, nil
174	}
175
176	// If we are not linting and have a cluster connection, provide a Kubernetes-backed
177	// implementation.
178	if !e.LintMode && e.config != nil {
179		funcMap["lookup"] = NewLookupFunction(e.config)
180	}
181
182	t.Funcs(funcMap)
183}
184
185// render takes a map of templates/values and renders them.
186func (e Engine) render(tpls map[string]renderable) (map[string]string, error) {
187	return e.renderWithReferences(tpls, tpls)
188}
189
190// renderWithReferences takes a map of templates/values to render, and a map of
191// templates which can be referenced within them.
192func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable) (rendered map[string]string, err error) {
193	// Basically, what we do here is start with an empty parent template and then
194	// build up a list of templates -- one for each file. Once all of the templates
195	// have been parsed, we loop through again and execute every template.
196	//
197	// The idea with this process is to make it possible for more complex templates
198	// to share common blocks, but to make the entire thing feel like a file-based
199	// template engine.
200	defer func() {
201		if r := recover(); r != nil {
202			err = errors.Errorf("rendering template failed: %v", r)
203		}
204	}()
205	t := template.New("gotpl")
206	if e.Strict {
207		t.Option("missingkey=error")
208	} else {
209		// Not that zero will attempt to add default values for types it knows,
210		// but will still emit <no value> for others. We mitigate that later.
211		t.Option("missingkey=zero")
212	}
213
214	e.initFunMap(t, referenceTpls)
215
216	// We want to parse the templates in a predictable order. The order favors
217	// higher-level (in file system) templates over deeply nested templates.
218	keys := sortTemplates(tpls)
219	referenceKeys := sortTemplates(referenceTpls)
220
221	for _, filename := range keys {
222		r := tpls[filename]
223		if _, err := t.New(filename).Parse(r.tpl); err != nil {
224			return map[string]string{}, cleanupParseError(filename, err)
225		}
226	}
227
228	// Adding the reference templates to the template context
229	// so they can be referenced in the tpl function
230	for _, filename := range referenceKeys {
231		if t.Lookup(filename) == nil {
232			r := referenceTpls[filename]
233			if _, err := t.New(filename).Parse(r.tpl); err != nil {
234				return map[string]string{}, cleanupParseError(filename, err)
235			}
236		}
237	}
238
239	rendered = make(map[string]string, len(keys))
240	for _, filename := range keys {
241		// Don't render partials. We don't care out the direct output of partials.
242		// They are only included from other templates.
243		if strings.HasPrefix(path.Base(filename), "_") {
244			continue
245		}
246		// At render time, add information about the template that is being rendered.
247		vals := tpls[filename].vals
248		vals["Template"] = chartutil.Values{"Name": filename, "BasePath": tpls[filename].basePath}
249		var buf strings.Builder
250		if err := t.ExecuteTemplate(&buf, filename, vals); err != nil {
251			return map[string]string{}, cleanupExecError(filename, err)
252		}
253
254		// Work around the issue where Go will emit "<no value>" even if Options(missing=zero)
255		// is set. Since missing=error will never get here, we do not need to handle
256		// the Strict case.
257		rendered[filename] = strings.ReplaceAll(buf.String(), "<no value>", "")
258	}
259
260	return rendered, nil
261}
262
263func cleanupParseError(filename string, err error) error {
264	tokens := strings.Split(err.Error(), ": ")
265	if len(tokens) == 1 {
266		// This might happen if a non-templating error occurs
267		return fmt.Errorf("parse error in (%s): %s", filename, err)
268	}
269	// The first token is "template"
270	// The second token is either "filename:lineno" or "filename:lineNo:columnNo"
271	location := tokens[1]
272	// The remaining tokens make up a stacktrace-like chain, ending with the relevant error
273	errMsg := tokens[len(tokens)-1]
274	return fmt.Errorf("parse error at (%s): %s", string(location), errMsg)
275}
276
277func cleanupExecError(filename string, err error) error {
278	if _, isExecError := err.(template.ExecError); !isExecError {
279		return err
280	}
281
282	tokens := strings.SplitN(err.Error(), ": ", 3)
283	if len(tokens) != 3 {
284		// This might happen if a non-templating error occurs
285		return fmt.Errorf("execution error in (%s): %s", filename, err)
286	}
287
288	// The first token is "template"
289	// The second token is either "filename:lineno" or "filename:lineNo:columnNo"
290	location := tokens[1]
291
292	parts := warnRegex.FindStringSubmatch(tokens[2])
293	if len(parts) >= 2 {
294		return fmt.Errorf("execution error at (%s): %s", string(location), parts[1])
295	}
296
297	return err
298}
299
300func sortTemplates(tpls map[string]renderable) []string {
301	keys := make([]string, len(tpls))
302	i := 0
303	for key := range tpls {
304		keys[i] = key
305		i++
306	}
307	sort.Sort(sort.Reverse(byPathLen(keys)))
308	return keys
309}
310
311type byPathLen []string
312
313func (p byPathLen) Len() int      { return len(p) }
314func (p byPathLen) Swap(i, j int) { p[j], p[i] = p[i], p[j] }
315func (p byPathLen) Less(i, j int) bool {
316	a, b := p[i], p[j]
317	ca, cb := strings.Count(a, "/"), strings.Count(b, "/")
318	if ca == cb {
319		return strings.Compare(a, b) == -1
320	}
321	return ca < cb
322}
323
324// allTemplates returns all templates for a chart and its dependencies.
325//
326// As it goes, it also prepares the values in a scope-sensitive manner.
327func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
328	templates := make(map[string]renderable)
329	recAllTpls(c, templates, vals)
330	return templates
331}
332
333// recAllTpls recurses through the templates in a chart.
334//
335// As it recurses, it also sets the values to be appropriate for the template
336// scope.
337func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) {
338	next := map[string]interface{}{
339		"Chart":        c.Metadata,
340		"Files":        newFiles(c.Files),
341		"Release":      vals["Release"],
342		"Capabilities": vals["Capabilities"],
343		"Values":       make(chartutil.Values),
344	}
345
346	// If there is a {{.Values.ThisChart}} in the parent metadata,
347	// copy that into the {{.Values}} for this template.
348	if c.IsRoot() {
349		next["Values"] = vals["Values"]
350	} else if vs, err := vals.Table("Values." + c.Name()); err == nil {
351		next["Values"] = vs
352	}
353
354	for _, child := range c.Dependencies() {
355		recAllTpls(child, templates, next)
356	}
357
358	newParentID := c.ChartFullPath()
359	for _, t := range c.Templates {
360		if !isTemplateValid(c, t.Name) {
361			continue
362		}
363		templates[path.Join(newParentID, t.Name)] = renderable{
364			tpl:      string(t.Data),
365			vals:     next,
366			basePath: path.Join(newParentID, "templates"),
367		}
368	}
369}
370
371// isTemplateValid returns true if the template is valid for the chart type
372func isTemplateValid(ch *chart.Chart, templateName string) bool {
373	if isLibraryChart(ch) {
374		return strings.HasPrefix(filepath.Base(templateName), "_")
375	}
376	return true
377}
378
379// isLibraryChart returns true if the chart is a library chart
380func isLibraryChart(c *chart.Chart) bool {
381	return strings.EqualFold(c.Metadata.Type, "library")
382}
383