github.com/x-helm/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  }