1
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 "k8s.io/client-go/rest"
31
32 "helm.sh/helm/v3/pkg/chart"
33 "helm.sh/helm/v3/pkg/chartutil"
34 )
35
36
37 type Engine struct {
38
39
40 Strict bool
41
42 LintMode bool
43
44 clientProvider *ClientProvider
45
46 EnableDNS bool
47 }
48
49
50 func New(config *rest.Config) Engine {
51 var clientProvider ClientProvider = clientProviderFromConfig{config}
52 return Engine{
53 clientProvider: &clientProvider,
54 }
55 }
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76 func (e Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
77 tmap := allTemplates(chrt, values)
78 return e.render(tmap)
79 }
80
81
82
83 func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
84 return new(Engine).Render(chrt, values)
85 }
86
87
88
89
90 func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config) (map[string]string, error) {
91 var clientProvider ClientProvider = clientProviderFromConfig{config}
92 return Engine{
93 clientProvider: &clientProvider,
94 }.Render(chrt, values)
95 }
96
97
98
99
100
101 func RenderWithClientProvider(chrt *chart.Chart, values chartutil.Values, clientProvider ClientProvider) (map[string]string, error) {
102 return Engine{
103 clientProvider: &clientProvider,
104 }.Render(chrt, values)
105 }
106
107
108 type renderable struct {
109
110 tpl string
111
112 vals chartutil.Values
113
114 basePath string
115 }
116
117 const warnStartDelim = "HELM_ERR_START"
118 const warnEndDelim = "HELM_ERR_END"
119 const recursionMaxNums = 1000
120
121 var warnRegex = regexp.MustCompile(warnStartDelim + `((?s).*)` + warnEndDelim)
122
123 func warnWrap(warn string) string {
124 return warnStartDelim + warn + warnEndDelim
125 }
126
127
128
129 func includeFun(t *template.Template, includedNames map[string]int) func(string, interface{}) (string, error) {
130 return func(name string, data interface{}) (string, error) {
131 var buf strings.Builder
132 if v, ok := includedNames[name]; ok {
133 if v > recursionMaxNums {
134 return "", errors.Wrapf(fmt.Errorf("unable to execute template"), "rendering template has a nested reference name: %s", name)
135 }
136 includedNames[name]++
137 } else {
138 includedNames[name] = 1
139 }
140 err := t.ExecuteTemplate(&buf, name, data)
141 includedNames[name]--
142 return buf.String(), err
143 }
144 }
145
146
147
148 func tplFun(parent *template.Template, includedNames map[string]int, strict bool) func(string, interface{}) (string, error) {
149 return func(tpl string, vals interface{}) (string, error) {
150 t, err := parent.Clone()
151 if err != nil {
152 return "", errors.Wrapf(err, "cannot clone template")
153 }
154
155
156
157
158 if strict {
159 t.Option("missingkey=error")
160 } else {
161 t.Option("missingkey=zero")
162 }
163
164
165
166 t.Funcs(template.FuncMap{
167 "include": includeFun(t, includedNames),
168 "tpl": tplFun(t, includedNames, strict),
169 })
170
171
172
173
174
175
176
177 t, err = t.New(parent.Name()).Parse(tpl)
178 if err != nil {
179 return "", errors.Wrapf(err, "cannot parse template %q", tpl)
180 }
181
182 var buf strings.Builder
183 if err := t.Execute(&buf, vals); err != nil {
184 return "", errors.Wrapf(err, "error during tpl function execution for %q", tpl)
185 }
186
187
188 return strings.ReplaceAll(buf.String(), "<no value>", ""), nil
189 }
190 }
191
192
193 func (e Engine) initFunMap(t *template.Template) {
194 funcMap := funcMap()
195 includedNames := make(map[string]int)
196
197
198 funcMap["include"] = includeFun(t, includedNames)
199 funcMap["tpl"] = tplFun(t, includedNames, e.Strict)
200
201
202 funcMap["required"] = func(warn string, val interface{}) (interface{}, error) {
203 if val == nil {
204 if e.LintMode {
205
206 log.Printf("[INFO] Missing required value: %s", warn)
207 return "", nil
208 }
209 return val, errors.Errorf(warnWrap(warn))
210 } else if _, ok := val.(string); ok {
211 if val == "" {
212 if e.LintMode {
213
214 log.Printf("[INFO] Missing required value: %s", warn)
215 return "", nil
216 }
217 return val, errors.Errorf(warnWrap(warn))
218 }
219 }
220 return val, nil
221 }
222
223
224 funcMap["fail"] = func(msg string) (string, error) {
225 if e.LintMode {
226
227 log.Printf("[INFO] Fail: %s", msg)
228 return "", nil
229 }
230 return "", errors.New(warnWrap(msg))
231 }
232
233
234
235 if !e.LintMode && e.clientProvider != nil {
236 funcMap["lookup"] = newLookupFunction(*e.clientProvider)
237 }
238
239
240
241 if !e.EnableDNS {
242 funcMap["getHostByName"] = func(_ string) string {
243 return ""
244 }
245 }
246
247 t.Funcs(funcMap)
248 }
249
250
251 func (e Engine) render(tpls map[string]renderable) (rendered map[string]string, err error) {
252
253
254
255
256
257
258
259 defer func() {
260 if r := recover(); r != nil {
261 err = errors.Errorf("rendering template failed: %v", r)
262 }
263 }()
264 t := template.New("gotpl")
265 if e.Strict {
266 t.Option("missingkey=error")
267 } else {
268
269
270 t.Option("missingkey=zero")
271 }
272
273 e.initFunMap(t)
274
275
276
277 keys := sortTemplates(tpls)
278
279 for _, filename := range keys {
280 r := tpls[filename]
281 if _, err := t.New(filename).Parse(r.tpl); err != nil {
282 return map[string]string{}, cleanupParseError(filename, err)
283 }
284 }
285
286 rendered = make(map[string]string, len(keys))
287 for _, filename := range keys {
288
289
290 if strings.HasPrefix(path.Base(filename), "_") {
291 continue
292 }
293
294 vals := tpls[filename].vals
295 vals["Template"] = chartutil.Values{"Name": filename, "BasePath": tpls[filename].basePath}
296 var buf strings.Builder
297 if err := t.ExecuteTemplate(&buf, filename, vals); err != nil {
298 return map[string]string{}, cleanupExecError(filename, err)
299 }
300
301
302
303
304 rendered[filename] = strings.ReplaceAll(buf.String(), "<no value>", "")
305 }
306
307 return rendered, nil
308 }
309
310 func cleanupParseError(filename string, err error) error {
311 tokens := strings.Split(err.Error(), ": ")
312 if len(tokens) == 1 {
313
314 return fmt.Errorf("parse error in (%s): %s", filename, err)
315 }
316
317
318 location := tokens[1]
319
320 errMsg := tokens[len(tokens)-1]
321 return fmt.Errorf("parse error at (%s): %s", string(location), errMsg)
322 }
323
324 func cleanupExecError(filename string, err error) error {
325 if _, isExecError := err.(template.ExecError); !isExecError {
326 return err
327 }
328
329 tokens := strings.SplitN(err.Error(), ": ", 3)
330 if len(tokens) != 3 {
331
332 return fmt.Errorf("execution error in (%s): %s", filename, err)
333 }
334
335
336
337 location := tokens[1]
338
339 parts := warnRegex.FindStringSubmatch(tokens[2])
340 if len(parts) >= 2 {
341 return fmt.Errorf("execution error at (%s): %s", string(location), parts[1])
342 }
343
344 return err
345 }
346
347 func sortTemplates(tpls map[string]renderable) []string {
348 keys := make([]string, len(tpls))
349 i := 0
350 for key := range tpls {
351 keys[i] = key
352 i++
353 }
354 sort.Sort(sort.Reverse(byPathLen(keys)))
355 return keys
356 }
357
358 type byPathLen []string
359
360 func (p byPathLen) Len() int { return len(p) }
361 func (p byPathLen) Swap(i, j int) { p[j], p[i] = p[i], p[j] }
362 func (p byPathLen) Less(i, j int) bool {
363 a, b := p[i], p[j]
364 ca, cb := strings.Count(a, "/"), strings.Count(b, "/")
365 if ca == cb {
366 return strings.Compare(a, b) == -1
367 }
368 return ca < cb
369 }
370
371
372
373
374 func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
375 templates := make(map[string]renderable)
376 recAllTpls(c, templates, vals)
377 return templates
378 }
379
380
381
382
383
384 func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) map[string]interface{} {
385 subCharts := make(map[string]interface{})
386 chartMetaData := struct {
387 chart.Metadata
388 IsRoot bool
389 }{*c.Metadata, c.IsRoot()}
390
391 next := map[string]interface{}{
392 "Chart": chartMetaData,
393 "Files": newFiles(c.Files),
394 "Release": vals["Release"],
395 "Capabilities": vals["Capabilities"],
396 "Values": make(chartutil.Values),
397 "Subcharts": subCharts,
398 }
399
400
401
402 if c.IsRoot() {
403 next["Values"] = vals["Values"]
404 } else if vs, err := vals.Table("Values." + c.Name()); err == nil {
405 next["Values"] = vs
406 }
407
408 for _, child := range c.Dependencies() {
409 subCharts[child.Name()] = recAllTpls(child, templates, next)
410 }
411
412 newParentID := c.ChartFullPath()
413 for _, t := range c.Templates {
414 if t == nil {
415 continue
416 }
417 if !isTemplateValid(c, t.Name) {
418 continue
419 }
420 templates[path.Join(newParentID, t.Name)] = renderable{
421 tpl: string(t.Data),
422 vals: next,
423 basePath: path.Join(newParentID, "templates"),
424 }
425 }
426
427 return next
428 }
429
430
431 func isTemplateValid(ch *chart.Chart, templateName string) bool {
432 if isLibraryChart(ch) {
433 return strings.HasPrefix(filepath.Base(templateName), "_")
434 }
435 return true
436 }
437
438
439 func isLibraryChart(c *chart.Chart) bool {
440 return strings.EqualFold(c.Metadata.Type, "library")
441 }
442
View as plain text