github.com/latiif/helm@v2.15.0+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 "bytes" 21 "fmt" 22 "log" 23 "path" 24 "sort" 25 "strings" 26 "text/template" 27 28 "github.com/Masterminds/sprig" 29 30 "k8s.io/helm/pkg/chartutil" 31 "k8s.io/helm/pkg/proto/hapi/chart" 32 ) 33 34 // Engine is an implementation of 'cmd/tiller/environment'.Engine that uses Go templates. 35 type Engine struct { 36 // FuncMap contains the template functions that will be passed to each 37 // render call. This may only be modified before the first call to Render. 38 FuncMap template.FuncMap 39 // If strict is enabled, template rendering will fail if a template references 40 // a value that was not passed in. 41 Strict bool 42 // In LintMode, some 'required' template values may be missing, so don't fail 43 LintMode bool 44 } 45 46 // New creates a new Go template Engine instance. 47 // 48 // The FuncMap is initialized here. You may modify the FuncMap _prior to_ the 49 // first invocation of Render. 50 // 51 // The FuncMap sets all of the Sprig functions except for those that provide 52 // access to the underlying OS (env, expandenv). 53 func New() *Engine { 54 f := FuncMap() 55 return &Engine{ 56 FuncMap: f, 57 } 58 } 59 60 // FuncMap returns a mapping of all of the functions that Engine has. 61 // 62 // Because some functions are late-bound (e.g. contain context-sensitive 63 // data), the functions may not all perform identically outside of an 64 // Engine as they will inside of an Engine. 65 // 66 // Known late-bound functions: 67 // 68 // - "include": This is late-bound in Engine.Render(). The version 69 // included in the FuncMap is a placeholder. 70 // - "required": This is late-bound in Engine.Render(). The version 71 // included in the FuncMap is a placeholder. 72 // - "tpl": This is late-bound in Engine.Render(). The version 73 // included in the FuncMap is a placeholder. 74 func FuncMap() template.FuncMap { 75 f := sprig.TxtFuncMap() 76 delete(f, "env") 77 delete(f, "expandenv") 78 79 // Add some extra functionality 80 extra := template.FuncMap{ 81 "toToml": chartutil.ToToml, 82 "toYaml": chartutil.ToYaml, 83 "fromYaml": chartutil.FromYaml, 84 "toJson": chartutil.ToJson, 85 "fromJson": chartutil.FromJson, 86 87 // This is a placeholder for the "include" function, which is 88 // late-bound to a template. By declaring it here, we preserve the 89 // integrity of the linter. 90 "include": func(string, interface{}) string { return "not implemented" }, 91 "required": func(string, interface{}) interface{} { return "not implemented" }, 92 "tpl": func(string, interface{}) interface{} { return "not implemented" }, 93 } 94 95 for k, v := range extra { 96 f[k] = v 97 } 98 99 return f 100 } 101 102 // Render takes a chart, optional values, and value overrides, and attempts to render the Go templates. 103 // 104 // Render can be called repeatedly on the same engine. 105 // 106 // This will look in the chart's 'templates' data (e.g. the 'templates/' directory) 107 // and attempt to render the templates there using the values passed in. 108 // 109 // Values are scoped to their templates. A dependency template will not have 110 // access to the values set for its parent. If chart "foo" includes chart "bar", 111 // "bar" will not have access to the values for "foo". 112 // 113 // Values should be prepared with something like `chartutils.ReadValues`. 114 // 115 // Values are passed through the templates according to scope. If the top layer 116 // chart includes the chart foo, which includes the chart bar, the values map 117 // will be examined for a table called "foo". If "foo" is found in vals, 118 // that section of the values will be passed into the "foo" chart. And if that 119 // section contains a value named "bar", that value will be passed on to the 120 // bar chart during render time. 121 func (e *Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) { 122 // Render the charts 123 tmap := allTemplates(chrt, values) 124 return e.render(tmap) 125 } 126 127 // renderable is an object that can be rendered. 128 type renderable struct { 129 // tpl is the current template. 130 tpl string 131 // vals are the values to be supplied to the template. 132 vals chartutil.Values 133 // basePath namespace prefix to the templates of the current chart 134 basePath string 135 } 136 137 // alterFuncMap takes the Engine's FuncMap and adds context-specific functions. 138 // 139 // The resulting FuncMap is only valid for the passed-in template. 140 func (e *Engine) alterFuncMap(t *template.Template, referenceTpls map[string]renderable) template.FuncMap { 141 // Clone the func map because we are adding context-specific functions. 142 var funcMap template.FuncMap = map[string]interface{}{} 143 for k, v := range e.FuncMap { 144 funcMap[k] = v 145 } 146 147 // Add the 'include' function here so we can close over t. 148 funcMap["include"] = func(name string, data interface{}) (string, error) { 149 buf := bytes.NewBuffer(nil) 150 if err := t.ExecuteTemplate(buf, name, data); err != nil { 151 return "", err 152 } 153 return buf.String(), nil 154 } 155 156 // Add the 'required' function here 157 funcMap["required"] = func(warn string, val interface{}) (interface{}, error) { 158 if val == nil { 159 if e.LintMode { 160 // Don't fail on missing required values when linting 161 log.Printf("[INFO] Missing required value: %s", warn) 162 return "", nil 163 } 164 // Convert nil to "" in case required is piped into other functions 165 return "", fmt.Errorf(warn) 166 } else if _, ok := val.(string); ok { 167 if val == "" { 168 if e.LintMode { 169 // Don't fail on missing required values when linting 170 log.Printf("[INFO] Missing required value: %s", warn) 171 return val, nil 172 } 173 return val, fmt.Errorf(warn) 174 } 175 } 176 return val, nil 177 } 178 179 // Add the 'tpl' function here 180 funcMap["tpl"] = func(tpl string, vals chartutil.Values) (string, error) { 181 basePath, err := vals.PathValue("Template.BasePath") 182 if err != nil { 183 return "", fmt.Errorf("Cannot retrieve Template.Basepath from values inside tpl function: %s (%s)", tpl, err.Error()) 184 } 185 186 r := renderable{ 187 tpl: tpl, 188 vals: vals, 189 basePath: basePath.(string), 190 } 191 192 templates := map[string]renderable{} 193 templateName, err := vals.PathValue("Template.Name") 194 if err != nil { 195 return "", fmt.Errorf("Cannot retrieve Template.Name from values inside tpl function: %s (%s)", tpl, err.Error()) 196 } 197 198 templates[templateName.(string)] = r 199 200 result, err := e.renderWithReferences(templates, referenceTpls) 201 if err != nil { 202 return "", fmt.Errorf("Error during tpl function execution for %q: %s", tpl, err.Error()) 203 } 204 return result[templateName.(string)], nil 205 } 206 207 return funcMap 208 } 209 210 // render takes a map of templates/values and renders them. 211 func (e *Engine) render(tpls map[string]renderable) (rendered map[string]string, err error) { 212 return e.renderWithReferences(tpls, tpls) 213 } 214 215 // renderWithReferences takes a map of templates/values to render, and a map of 216 // templates which can be referenced within them. 217 func (e *Engine) renderWithReferences(tpls map[string]renderable, referenceTpls map[string]renderable) (rendered map[string]string, err error) { 218 // Basically, what we do here is start with an empty parent template and then 219 // build up a list of templates -- one for each file. Once all of the templates 220 // have been parsed, we loop through again and execute every template. 221 // 222 // The idea with this process is to make it possible for more complex templates 223 // to share common blocks, but to make the entire thing feel like a file-based 224 // template engine. 225 defer func() { 226 if r := recover(); r != nil { 227 err = fmt.Errorf("rendering template failed: %v", r) 228 } 229 }() 230 t := template.New("gotpl") 231 if e.Strict { 232 t.Option("missingkey=error") 233 } else { 234 // Not that zero will attempt to add default values for types it knows, 235 // but will still emit <no value> for others. We mitigate that later. 236 t.Option("missingkey=zero") 237 } 238 239 funcMap := e.alterFuncMap(t, referenceTpls) 240 241 // We want to parse the templates in a predictable order. The order favors 242 // higher-level (in file system) templates over deeply nested templates. 243 keys := sortTemplates(tpls) 244 245 files := []string{} 246 247 for _, fname := range keys { 248 r := tpls[fname] 249 t = t.New(fname).Funcs(funcMap) 250 if _, err := t.Parse(r.tpl); err != nil { 251 return map[string]string{}, fmt.Errorf("parse error in %q: %s", fname, err) 252 } 253 files = append(files, fname) 254 } 255 256 // Adding the reference templates to the template context 257 // so they can be referenced in the tpl function 258 for fname, r := range referenceTpls { 259 if t.Lookup(fname) == nil { 260 t = t.New(fname).Funcs(funcMap) 261 if _, err := t.Parse(r.tpl); err != nil { 262 return map[string]string{}, fmt.Errorf("parse error in %q: %s", fname, err) 263 } 264 } 265 } 266 267 rendered = make(map[string]string, len(files)) 268 var buf bytes.Buffer 269 for _, file := range files { 270 // Don't render partials. We don't care about the direct output of partials. 271 // They are only included from other templates. 272 if strings.HasPrefix(path.Base(file), "_") { 273 continue 274 } 275 // At render time, add information about the template that is being rendered. 276 vals := tpls[file].vals 277 vals["Template"] = map[string]interface{}{"Name": file, "BasePath": tpls[file].basePath} 278 if err := t.ExecuteTemplate(&buf, file, vals); err != nil { 279 return map[string]string{}, fmt.Errorf("render error in %q: %s", file, err) 280 } 281 282 // Work around the issue where Go will emit "<no value>" even if Options(missing=zero) 283 // is set. Since missing=error will never get here, we do not need to handle 284 // the Strict case. 285 rendered[file] = strings.Replace(buf.String(), "<no value>", "", -1) 286 buf.Reset() 287 } 288 289 return rendered, nil 290 } 291 292 func sortTemplates(tpls map[string]renderable) []string { 293 keys := make([]string, len(tpls)) 294 i := 0 295 for key := range tpls { 296 keys[i] = key 297 i++ 298 } 299 sort.Sort(sort.Reverse(byPathLen(keys))) 300 return keys 301 } 302 303 type byPathLen []string 304 305 func (p byPathLen) Len() int { return len(p) } 306 func (p byPathLen) Swap(i, j int) { p[j], p[i] = p[i], p[j] } 307 func (p byPathLen) Less(i, j int) bool { 308 a, b := p[i], p[j] 309 ca, cb := strings.Count(a, "/"), strings.Count(b, "/") 310 if ca == cb { 311 return strings.Compare(a, b) == -1 312 } 313 return ca < cb 314 } 315 316 // allTemplates returns all templates for a chart and its dependencies. 317 // 318 // As it goes, it also prepares the values in a scope-sensitive manner. 319 func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable { 320 templates := map[string]renderable{} 321 recAllTpls(c, templates, vals, true, "") 322 return templates 323 } 324 325 // recAllTpls recurses through the templates in a chart. 326 // 327 // As it recurses, it also sets the values to be appropriate for the template 328 // scope. 329 func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals chartutil.Values, top bool, parentID string) { 330 // This should never evaluate to a nil map. That will cause problems when 331 // values are appended later. 332 cvals := chartutil.Values{} 333 if top { 334 // If this is the top of the rendering tree, assume that parentVals 335 // is already resolved to the authoritative values. 336 cvals = parentVals 337 } else if c.Metadata != nil && c.Metadata.Name != "" { 338 // If there is a {{.Values.ThisChart}} in the parent metadata, 339 // copy that into the {{.Values}} for this template. 340 newVals := chartutil.Values{} 341 if vs, err := parentVals.Table("Values"); err == nil { 342 if tmp, err := vs.Table(c.Metadata.Name); err == nil { 343 newVals = tmp 344 } 345 } 346 347 cvals = map[string]interface{}{ 348 "Values": newVals, 349 "Release": parentVals["Release"], 350 "Chart": c.Metadata, 351 "Files": chartutil.NewFiles(c.Files), 352 "Capabilities": parentVals["Capabilities"], 353 } 354 } 355 356 newParentID := c.Metadata.Name 357 if parentID != "" { 358 // We artificially reconstruct the chart path to child templates. This 359 // creates a namespaced filename that can be used to track down the source 360 // of a particular template declaration. 361 newParentID = path.Join(parentID, "charts", newParentID) 362 } 363 364 for _, child := range c.Dependencies { 365 recAllTpls(child, templates, cvals, false, newParentID) 366 } 367 for _, t := range c.Templates { 368 templates[path.Join(newParentID, t.Name)] = renderable{ 369 tpl: string(t.Data), 370 vals: cvals, 371 basePath: path.Join(newParentID, "templates"), 372 } 373 } 374 }