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