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