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