github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/engine/engine.go (about) 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 "k8s.io/client-go/rest" 31 32 "github.com/stefanmcshane/helm/pkg/chart" 33 "github.com/stefanmcshane/helm/pkg/chartutil" 34 ) 35 36 // Engine is an implementation of the Helm rendering implementation for templates. 37 type 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. 66 func (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. 73 func 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 80 func 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. 87 type 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 96 const warnStartDelim = "HELM_ERR_START" 97 const warnEndDelim = "HELM_ERR_END" 98 const recursionMaxNums = 1000 99 100 var warnRegex = regexp.MustCompile(warnStartDelim + `((?s).*)` + warnEndDelim) 101 102 func warnWrap(warn string) string { 103 return warnStartDelim + warn + warnEndDelim 104 } 105 106 // initFunMap creates the Engine's FuncMap and adds context-specific functions. 107 func (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 // Override sprig fail function for linting and wrapping message 177 funcMap["fail"] = func(msg string) (string, error) { 178 if e.LintMode { 179 // Don't fail when linting 180 log.Printf("[INFO] Fail: %s", msg) 181 return "", nil 182 } 183 return "", errors.New(warnWrap(msg)) 184 } 185 186 // If we are not linting and have a cluster connection, provide a Kubernetes-backed 187 // implementation. 188 if !e.LintMode && e.config != nil { 189 funcMap["lookup"] = NewLookupFunction(e.config) 190 } 191 192 t.Funcs(funcMap) 193 } 194 195 // render takes a map of templates/values and renders them. 196 func (e Engine) render(tpls map[string]renderable) (map[string]string, error) { 197 return e.renderWithReferences(tpls, tpls) 198 } 199 200 // renderWithReferences takes a map of templates/values to render, and a map of 201 // templates which can be referenced within them. 202 func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable) (rendered map[string]string, err error) { 203 // Basically, what we do here is start with an empty parent template and then 204 // build up a list of templates -- one for each file. Once all of the templates 205 // have been parsed, we loop through again and execute every template. 206 // 207 // The idea with this process is to make it possible for more complex templates 208 // to share common blocks, but to make the entire thing feel like a file-based 209 // template engine. 210 defer func() { 211 if r := recover(); r != nil { 212 err = errors.Errorf("rendering template failed: %v", r) 213 } 214 }() 215 t := template.New("gotpl") 216 if e.Strict { 217 t.Option("missingkey=error") 218 } else { 219 // Not that zero will attempt to add default values for types it knows, 220 // but will still emit <no value> for others. We mitigate that later. 221 t.Option("missingkey=zero") 222 } 223 224 e.initFunMap(t, referenceTpls) 225 226 // We want to parse the templates in a predictable order. The order favors 227 // higher-level (in file system) templates over deeply nested templates. 228 keys := sortTemplates(tpls) 229 referenceKeys := sortTemplates(referenceTpls) 230 231 for _, filename := range keys { 232 r := tpls[filename] 233 if _, err := t.New(filename).Parse(r.tpl); err != nil { 234 return map[string]string{}, cleanupParseError(filename, err) 235 } 236 } 237 238 // Adding the reference templates to the template context 239 // so they can be referenced in the tpl function 240 for _, filename := range referenceKeys { 241 if t.Lookup(filename) == nil { 242 r := referenceTpls[filename] 243 if _, err := t.New(filename).Parse(r.tpl); err != nil { 244 return map[string]string{}, cleanupParseError(filename, err) 245 } 246 } 247 } 248 249 rendered = make(map[string]string, len(keys)) 250 for _, filename := range keys { 251 // Don't render partials. We don't care out the direct output of partials. 252 // They are only included from other templates. 253 if strings.HasPrefix(path.Base(filename), "_") { 254 continue 255 } 256 // At render time, add information about the template that is being rendered. 257 vals := tpls[filename].vals 258 vals["Template"] = chartutil.Values{"Name": filename, "BasePath": tpls[filename].basePath} 259 var buf strings.Builder 260 if err := t.ExecuteTemplate(&buf, filename, vals); err != nil { 261 return map[string]string{}, cleanupExecError(filename, err) 262 } 263 264 // Work around the issue where Go will emit "<no value>" even if Options(missing=zero) 265 // is set. Since missing=error will never get here, we do not need to handle 266 // the Strict case. 267 rendered[filename] = strings.ReplaceAll(buf.String(), "<no value>", "") 268 } 269 270 return rendered, nil 271 } 272 273 func cleanupParseError(filename string, err error) error { 274 tokens := strings.Split(err.Error(), ": ") 275 if len(tokens) == 1 { 276 // This might happen if a non-templating error occurs 277 return fmt.Errorf("parse error in (%s): %s", filename, err) 278 } 279 // The first token is "template" 280 // The second token is either "filename:lineno" or "filename:lineNo:columnNo" 281 location := tokens[1] 282 // The remaining tokens make up a stacktrace-like chain, ending with the relevant error 283 errMsg := tokens[len(tokens)-1] 284 return fmt.Errorf("parse error at (%s): %s", string(location), errMsg) 285 } 286 287 func cleanupExecError(filename string, err error) error { 288 if _, isExecError := err.(template.ExecError); !isExecError { 289 return err 290 } 291 292 tokens := strings.SplitN(err.Error(), ": ", 3) 293 if len(tokens) != 3 { 294 // This might happen if a non-templating error occurs 295 return fmt.Errorf("execution error in (%s): %s", filename, err) 296 } 297 298 // The first token is "template" 299 // The second token is either "filename:lineno" or "filename:lineNo:columnNo" 300 location := tokens[1] 301 302 parts := warnRegex.FindStringSubmatch(tokens[2]) 303 if len(parts) >= 2 { 304 return fmt.Errorf("execution error at (%s): %s", string(location), parts[1]) 305 } 306 307 return err 308 } 309 310 func sortTemplates(tpls map[string]renderable) []string { 311 keys := make([]string, len(tpls)) 312 i := 0 313 for key := range tpls { 314 keys[i] = key 315 i++ 316 } 317 sort.Sort(sort.Reverse(byPathLen(keys))) 318 return keys 319 } 320 321 type byPathLen []string 322 323 func (p byPathLen) Len() int { return len(p) } 324 func (p byPathLen) Swap(i, j int) { p[j], p[i] = p[i], p[j] } 325 func (p byPathLen) Less(i, j int) bool { 326 a, b := p[i], p[j] 327 ca, cb := strings.Count(a, "/"), strings.Count(b, "/") 328 if ca == cb { 329 return strings.Compare(a, b) == -1 330 } 331 return ca < cb 332 } 333 334 // allTemplates returns all templates for a chart and its dependencies. 335 // 336 // As it goes, it also prepares the values in a scope-sensitive manner. 337 func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable { 338 templates := make(map[string]renderable) 339 recAllTpls(c, templates, vals) 340 return templates 341 } 342 343 // recAllTpls recurses through the templates in a chart. 344 // 345 // As it recurses, it also sets the values to be appropriate for the template 346 // scope. 347 func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) map[string]interface{} { 348 subCharts := make(map[string]interface{}) 349 chartMetaData := struct { 350 chart.Metadata 351 IsRoot bool 352 }{*c.Metadata, c.IsRoot()} 353 354 next := map[string]interface{}{ 355 "Chart": chartMetaData, 356 "Files": newFiles(c.Files), 357 "Release": vals["Release"], 358 "Capabilities": vals["Capabilities"], 359 "Values": make(chartutil.Values), 360 "Subcharts": subCharts, 361 } 362 363 // If there is a {{.Values.ThisChart}} in the parent metadata, 364 // copy that into the {{.Values}} for this template. 365 if c.IsRoot() { 366 next["Values"] = vals["Values"] 367 } else if vs, err := vals.Table("Values." + c.Name()); err == nil { 368 next["Values"] = vs 369 } 370 371 for _, child := range c.Dependencies() { 372 subCharts[child.Name()] = recAllTpls(child, templates, next) 373 } 374 375 newParentID := c.ChartFullPath() 376 for _, t := range c.Templates { 377 if !isTemplateValid(c, t.Name) { 378 continue 379 } 380 templates[path.Join(newParentID, t.Name)] = renderable{ 381 tpl: string(t.Data), 382 vals: next, 383 basePath: path.Join(newParentID, "templates"), 384 } 385 } 386 387 return next 388 } 389 390 // isTemplateValid returns true if the template is valid for the chart type 391 func isTemplateValid(ch *chart.Chart, templateName string) bool { 392 if isLibraryChart(ch) { 393 return strings.HasPrefix(filepath.Base(templateName), "_") 394 } 395 return true 396 } 397 398 // isLibraryChart returns true if the chart is a library chart 399 func isLibraryChart(c *chart.Chart) bool { 400 return strings.EqualFold(c.Metadata.Type, "library") 401 }