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