talm

Форк
0
/
engine.go 
416 строк · 13.5 Кб
1
/*
2
Copyright The Helm Authors.
3

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
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

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.
15
*/
16

17
package engine
18

19
import (
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

31
	"helm.sh/helm/v3/pkg/chart"
32
	"helm.sh/helm/v3/pkg/chartutil"
33
)
34

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
38
}
39

40
// Engine is an implementation of the Helm rendering implementation for templates.
41
type Engine struct {
42
	// If strict is enabled, template rendering will fail if a template references
43
	// a value that was not passed in.
44
	Strict bool
45
	// In LintMode, some 'required' template values may be missing, so don't fail
46
	LintMode bool
47
	// EnableDNS tells the engine to allow DNS lookups when rendering templates
48
	EnableDNS bool
49
}
50

51
// Render takes a chart, optional values, and value overrides, and attempts to render the Go templates.
52
//
53
// Render can be called repeatedly on the same engine.
54
//
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.
57
//
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".
61
//
62
// Values should be prepared with something like `chartutils.ReadValues`.
63
//
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)
72
	return e.render(tmap)
73
}
74

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)
79
}
80

81
// renderable is an object that can be rendered.
82
type renderable struct {
83
	// tpl is the current template.
84
	tpl string
85
	// vals are the values to be supplied to the template.
86
	vals chartutil.Values
87
	// namespace prefix to the templates of the current chart
88
	basePath string
89
}
90

91
const warnStartDelim = "HELM_ERR_START"
92
const warnEndDelim = "HELM_ERR_END"
93
const recursionMaxNums = 1000
94

95
var warnRegex = regexp.MustCompile(warnStartDelim + `((?s).*)` + warnEndDelim)
96

97
func warnWrap(warn string) string {
98
	return warnStartDelim + warn + warnEndDelim
99
}
100

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)
109
			}
110
			includedNames[name]++
111
		} else {
112
			includedNames[name] = 1
113
		}
114
		err := t.ExecuteTemplate(&buf, name, data)
115
		includedNames[name]--
116
		return buf.String(), err
117
	}
118
}
119

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()
125
		if err != nil {
126
			return "", errors.Wrapf(err, "cannot clone template")
127
		}
128

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.
132
		if strict {
133
			t.Option("missingkey=error")
134
		} else {
135
			t.Option("missingkey=zero")
136
		}
137

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),
143
		})
144

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)
152
		if err != nil {
153
			return "", errors.Wrapf(err, "cannot parse template %q", tpl)
154
		}
155

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)
159
		}
160

161
		// See comment in renderWithReferences explaining the <no value> hack.
162
		return strings.ReplaceAll(buf.String(), "<no value>", ""), nil
163
	}
164
}
165

166
// initFunMap creates the Engine's FuncMap and adds context-specific functions.
167
func (e Engine) initFunMap(t *template.Template) {
168
	funcMap := funcMap()
169
	includedNames := make(map[string]int)
170

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)
174

175
	// Add the `required` function here so we can use lintMode
176
	funcMap["required"] = func(warn string, val interface{}) (interface{}, error) {
177
		if val == nil {
178
			if e.LintMode {
179
				// Don't fail on missing required values when linting
180
				log.Printf("[INFO] Missing required value: %s", warn)
181
				return "", nil
182
			}
183
			return val, errors.Errorf(warnWrap(warn))
184
		} else if _, ok := val.(string); ok {
185
			if val == "" {
186
				if e.LintMode {
187
					// Don't fail on missing required values when linting
188
					log.Printf("[INFO] Missing required value: %s", warn)
189
					return "", nil
190
				}
191
				return val, errors.Errorf(warnWrap(warn))
192
			}
193
		}
194
		return val, nil
195
	}
196

197
	// Override sprig fail function for linting and wrapping message
198
	funcMap["fail"] = func(msg string) (string, error) {
199
		if e.LintMode {
200
			// Don't fail when linting
201
			log.Printf("[INFO] Fail: %s", msg)
202
			return "", nil
203
		}
204
		return "", errors.New(warnWrap(msg))
205
	}
206

207
	// If we are not linting and have a cluster connection, provide a Kubernetes-backed
208
	// implementation.
209
	if !e.LintMode {
210
		funcMap["lookup"] = LookupFunc
211
	}
212

213
	// When DNS lookups are not enabled override the sprig function and return
214
	// an empty string.
215
	if !e.EnableDNS {
216
		funcMap["getHostByName"] = func(name string) string {
217
			return ""
218
		}
219
	}
220

221
	t.Funcs(funcMap)
222
}
223

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.
229
	//
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
232
	// template engine.
233
	defer func() {
234
		if r := recover(); r != nil {
235
			err = errors.Errorf("rendering template failed: %v", r)
236
		}
237
	}()
238
	t := template.New("gotpl")
239
	if e.Strict {
240
		t.Option("missingkey=error")
241
	} else {
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")
245
	}
246

247
	e.initFunMap(t)
248

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)
252

253
	for _, filename := range keys {
254
		r := tpls[filename]
255
		if _, err := t.New(filename).Parse(r.tpl); err != nil {
256
			return map[string]string{}, cleanupParseError(filename, err)
257
		}
258
	}
259

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), "_") {
265
			continue
266
		}
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)
273
		}
274

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
277
		// the Strict case.
278
		rendered[filename] = strings.ReplaceAll(buf.String(), "<no value>", "")
279
	}
280

281
	return rendered, nil
282
}
283

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)
289
	}
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)
296
}
297

298
func cleanupExecError(filename string, err error) error {
299
	if _, isExecError := err.(template.ExecError); !isExecError {
300
		return err
301
	}
302

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)
307
	}
308

309
	// The first token is "template"
310
	// The second token is either "filename:lineno" or "filename:lineNo:columnNo"
311
	location := tokens[1]
312

313
	parts := warnRegex.FindStringSubmatch(tokens[2])
314
	if len(parts) >= 2 {
315
		return fmt.Errorf("execution error at (%s): %s", string(location), parts[1])
316
	}
317

318
	return err
319
}
320

321
func sortTemplates(tpls map[string]renderable) []string {
322
	keys := make([]string, len(tpls))
323
	i := 0
324
	for key := range tpls {
325
		keys[i] = key
326
		i++
327
	}
328
	sort.Sort(sort.Reverse(byPathLen(keys)))
329
	return keys
330
}
331

332
type byPathLen []string
333

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 {
337
	a, b := p[i], p[j]
338
	ca, cb := strings.Count(a, "/"), strings.Count(b, "/")
339
	if ca == cb {
340
		return strings.Compare(a, b) == -1
341
	}
342
	return ca < cb
343
}
344

345
// allTemplates returns all templates for a chart and its dependencies.
346
//
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)
351
	return templates
352
}
353

354
// recAllTpls recurses through the templates in a chart.
355
//
356
// As it recurses, it also sets the values to be appropriate for the template
357
// scope.
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 {
361
		chart.Metadata
362
		IsRoot bool
363
	}{*c.Metadata, c.IsRoot()}
364

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,
372
		"Disks":        Disks,
373
	}
374

375
	// If there is a {{.Values.ThisChart}} in the parent metadata,
376
	// copy that into the {{.Values}} for this template.
377
	if c.IsRoot() {
378
		next["Values"] = vals["Values"]
379
	} else if vs, err := vals.Table("Values." + c.Name()); err == nil {
380
		next["Values"] = vs
381
	}
382

383
	for _, child := range c.Dependencies() {
384
		subCharts[child.Name()] = recAllTpls(child, templates, next)
385
	}
386

387
	newParentID := c.ChartFullPath()
388
	for _, t := range c.Templates {
389
		if t == nil {
390
			continue
391
		}
392
		if !isTemplateValid(c, t.Name) {
393
			continue
394
		}
395
		templates[path.Join(newParentID, t.Name)] = renderable{
396
			tpl:      string(t.Data),
397
			vals:     next,
398
			basePath: path.Join(newParentID, "templates"),
399
		}
400
	}
401

402
	return next
403
}
404

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), "_")
409
	}
410
	return true
411
}
412

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")
416
}
417

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.