...

Source file src/helm.sh/helm/v3/pkg/engine/engine.go

Documentation: helm.sh/helm/v3/pkg/engine

     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  	"k8s.io/client-go/rest"
    31  
    32  	"helm.sh/helm/v3/pkg/chart"
    33  	"helm.sh/helm/v3/pkg/chartutil"
    34  )
    35  
    36  // Engine is an implementation of the Helm rendering implementation for templates.
    37  type Engine struct {
    38  	// If strict is enabled, template rendering will fail if a template references
    39  	// a value that was not passed in.
    40  	Strict bool
    41  	// In LintMode, some 'required' template values may be missing, so don't fail
    42  	LintMode bool
    43  	// optional provider of clients to talk to the Kubernetes API
    44  	clientProvider *ClientProvider
    45  	// EnableDNS tells the engine to allow DNS lookups when rendering templates
    46  	EnableDNS bool
    47  }
    48  
    49  // New creates a new instance of Engine using the passed in rest config.
    50  func New(config *rest.Config) Engine {
    51  	var clientProvider ClientProvider = clientProviderFromConfig{config}
    52  	return Engine{
    53  		clientProvider: &clientProvider,
    54  	}
    55  }
    56  
    57  // Render takes a chart, optional values, and value overrides, and attempts to render the Go templates.
    58  //
    59  // Render can be called repeatedly on the same engine.
    60  //
    61  // This will look in the chart's 'templates' data (e.g. the 'templates/' directory)
    62  // and attempt to render the templates there using the values passed in.
    63  //
    64  // Values are scoped to their templates. A dependency template will not have
    65  // access to the values set for its parent. If chart "foo" includes chart "bar",
    66  // "bar" will not have access to the values for "foo".
    67  //
    68  // Values should be prepared with something like `chartutils.ReadValues`.
    69  //
    70  // Values are passed through the templates according to scope. If the top layer
    71  // chart includes the chart foo, which includes the chart bar, the values map
    72  // will be examined for a table called "foo". If "foo" is found in vals,
    73  // that section of the values will be passed into the "foo" chart. And if that
    74  // section contains a value named "bar", that value will be passed on to the
    75  // bar chart during render time.
    76  func (e Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
    77  	tmap := allTemplates(chrt, values)
    78  	return e.render(tmap)
    79  }
    80  
    81  // Render takes a chart, optional values, and value overrides, and attempts to
    82  // render the Go templates using the default options.
    83  func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
    84  	return new(Engine).Render(chrt, values)
    85  }
    86  
    87  // RenderWithClient takes a chart, optional values, and value overrides, and attempts to
    88  // render the Go templates using the default options. This engine is client aware and so can have template
    89  // functions that interact with the client.
    90  func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config) (map[string]string, error) {
    91  	var clientProvider ClientProvider = clientProviderFromConfig{config}
    92  	return Engine{
    93  		clientProvider: &clientProvider,
    94  	}.Render(chrt, values)
    95  }
    96  
    97  // RenderWithClientProvider takes a chart, optional values, and value overrides, and attempts to
    98  // render the Go templates using the default options. This engine is client aware and so can have template
    99  // functions that interact with the client.
   100  // This function differs from RenderWithClient in that it lets you customize the way a dynamic client is constructed.
   101  func RenderWithClientProvider(chrt *chart.Chart, values chartutil.Values, clientProvider ClientProvider) (map[string]string, error) {
   102  	return Engine{
   103  		clientProvider: &clientProvider,
   104  	}.Render(chrt, values)
   105  }
   106  
   107  // renderable is an object that can be rendered.
   108  type renderable struct {
   109  	// tpl is the current template.
   110  	tpl string
   111  	// vals are the values to be supplied to the template.
   112  	vals chartutil.Values
   113  	// namespace prefix to the templates of the current chart
   114  	basePath string
   115  }
   116  
   117  const warnStartDelim = "HELM_ERR_START"
   118  const warnEndDelim = "HELM_ERR_END"
   119  const recursionMaxNums = 1000
   120  
   121  var warnRegex = regexp.MustCompile(warnStartDelim + `((?s).*)` + warnEndDelim)
   122  
   123  func warnWrap(warn string) string {
   124  	return warnStartDelim + warn + warnEndDelim
   125  }
   126  
   127  // 'include' needs to be defined in the scope of a 'tpl' template as
   128  // well as regular file-loaded templates.
   129  func includeFun(t *template.Template, includedNames map[string]int) func(string, interface{}) (string, error) {
   130  	return func(name string, data interface{}) (string, error) {
   131  		var buf strings.Builder
   132  		if v, ok := includedNames[name]; ok {
   133  			if v > recursionMaxNums {
   134  				return "", errors.Wrapf(fmt.Errorf("unable to execute template"), "rendering template has a nested reference name: %s", name)
   135  			}
   136  			includedNames[name]++
   137  		} else {
   138  			includedNames[name] = 1
   139  		}
   140  		err := t.ExecuteTemplate(&buf, name, data)
   141  		includedNames[name]--
   142  		return buf.String(), err
   143  	}
   144  }
   145  
   146  // As does 'tpl', so that nested calls to 'tpl' see the templates
   147  // defined by their enclosing contexts.
   148  func tplFun(parent *template.Template, includedNames map[string]int, strict bool) func(string, interface{}) (string, error) {
   149  	return func(tpl string, vals interface{}) (string, error) {
   150  		t, err := parent.Clone()
   151  		if err != nil {
   152  			return "", errors.Wrapf(err, "cannot clone template")
   153  		}
   154  
   155  		// Re-inject the missingkey option, see text/template issue https://github.com/golang/go/issues/43022
   156  		// We have to go by strict from our engine configuration, as the option fields are private in Template.
   157  		// TODO: Remove workaround (and the strict parameter) once we build only with golang versions with a fix.
   158  		if strict {
   159  			t.Option("missingkey=error")
   160  		} else {
   161  			t.Option("missingkey=zero")
   162  		}
   163  
   164  		// Re-inject 'include' so that it can close over our clone of t;
   165  		// this lets any 'define's inside tpl be 'include'd.
   166  		t.Funcs(template.FuncMap{
   167  			"include": includeFun(t, includedNames),
   168  			"tpl":     tplFun(t, includedNames, strict),
   169  		})
   170  
   171  		// We need a .New template, as template text which is just blanks
   172  		// or comments after parsing out defines just addes new named
   173  		// template definitions without changing the main template.
   174  		// https://pkg.go.dev/text/template#Template.Parse
   175  		// Use the parent's name for lack of a better way to identify the tpl
   176  		// text string. (Maybe we could use a hash appended to the name?)
   177  		t, err = t.New(parent.Name()).Parse(tpl)
   178  		if err != nil {
   179  			return "", errors.Wrapf(err, "cannot parse template %q", tpl)
   180  		}
   181  
   182  		var buf strings.Builder
   183  		if err := t.Execute(&buf, vals); err != nil {
   184  			return "", errors.Wrapf(err, "error during tpl function execution for %q", tpl)
   185  		}
   186  
   187  		// See comment in renderWithReferences explaining the <no value> hack.
   188  		return strings.ReplaceAll(buf.String(), "<no value>", ""), nil
   189  	}
   190  }
   191  
   192  // initFunMap creates the Engine's FuncMap and adds context-specific functions.
   193  func (e Engine) initFunMap(t *template.Template) {
   194  	funcMap := funcMap()
   195  	includedNames := make(map[string]int)
   196  
   197  	// Add the template-rendering functions here so we can close over t.
   198  	funcMap["include"] = includeFun(t, includedNames)
   199  	funcMap["tpl"] = tplFun(t, includedNames, e.Strict)
   200  
   201  	// Add the `required` function here so we can use lintMode
   202  	funcMap["required"] = func(warn string, val interface{}) (interface{}, error) {
   203  		if val == nil {
   204  			if e.LintMode {
   205  				// Don't fail on missing required values when linting
   206  				log.Printf("[INFO] Missing required value: %s", warn)
   207  				return "", nil
   208  			}
   209  			return val, errors.Errorf(warnWrap(warn))
   210  		} else if _, ok := val.(string); ok {
   211  			if val == "" {
   212  				if e.LintMode {
   213  					// Don't fail on missing required values when linting
   214  					log.Printf("[INFO] Missing required value: %s", warn)
   215  					return "", nil
   216  				}
   217  				return val, errors.Errorf(warnWrap(warn))
   218  			}
   219  		}
   220  		return val, nil
   221  	}
   222  
   223  	// Override sprig fail function for linting and wrapping message
   224  	funcMap["fail"] = func(msg string) (string, error) {
   225  		if e.LintMode {
   226  			// Don't fail when linting
   227  			log.Printf("[INFO] Fail: %s", msg)
   228  			return "", nil
   229  		}
   230  		return "", errors.New(warnWrap(msg))
   231  	}
   232  
   233  	// If we are not linting and have a cluster connection, provide a Kubernetes-backed
   234  	// implementation.
   235  	if !e.LintMode && e.clientProvider != nil {
   236  		funcMap["lookup"] = newLookupFunction(*e.clientProvider)
   237  	}
   238  
   239  	// When DNS lookups are not enabled override the sprig function and return
   240  	// an empty string.
   241  	if !e.EnableDNS {
   242  		funcMap["getHostByName"] = func(_ string) string {
   243  			return ""
   244  		}
   245  	}
   246  
   247  	t.Funcs(funcMap)
   248  }
   249  
   250  // render takes a map of templates/values and renders them.
   251  func (e Engine) render(tpls map[string]renderable) (rendered map[string]string, err error) {
   252  	// Basically, what we do here is start with an empty parent template and then
   253  	// build up a list of templates -- one for each file. Once all of the templates
   254  	// have been parsed, we loop through again and execute every template.
   255  	//
   256  	// The idea with this process is to make it possible for more complex templates
   257  	// to share common blocks, but to make the entire thing feel like a file-based
   258  	// template engine.
   259  	defer func() {
   260  		if r := recover(); r != nil {
   261  			err = errors.Errorf("rendering template failed: %v", r)
   262  		}
   263  	}()
   264  	t := template.New("gotpl")
   265  	if e.Strict {
   266  		t.Option("missingkey=error")
   267  	} else {
   268  		// Not that zero will attempt to add default values for types it knows,
   269  		// but will still emit <no value> for others. We mitigate that later.
   270  		t.Option("missingkey=zero")
   271  	}
   272  
   273  	e.initFunMap(t)
   274  
   275  	// We want to parse the templates in a predictable order. The order favors
   276  	// higher-level (in file system) templates over deeply nested templates.
   277  	keys := sortTemplates(tpls)
   278  
   279  	for _, filename := range keys {
   280  		r := tpls[filename]
   281  		if _, err := t.New(filename).Parse(r.tpl); err != nil {
   282  			return map[string]string{}, cleanupParseError(filename, err)
   283  		}
   284  	}
   285  
   286  	rendered = make(map[string]string, len(keys))
   287  	for _, filename := range keys {
   288  		// Don't render partials. We don't care out the direct output of partials.
   289  		// They are only included from other templates.
   290  		if strings.HasPrefix(path.Base(filename), "_") {
   291  			continue
   292  		}
   293  		// At render time, add information about the template that is being rendered.
   294  		vals := tpls[filename].vals
   295  		vals["Template"] = chartutil.Values{"Name": filename, "BasePath": tpls[filename].basePath}
   296  		var buf strings.Builder
   297  		if err := t.ExecuteTemplate(&buf, filename, vals); err != nil {
   298  			return map[string]string{}, cleanupExecError(filename, err)
   299  		}
   300  
   301  		// Work around the issue where Go will emit "<no value>" even if Options(missing=zero)
   302  		// is set. Since missing=error will never get here, we do not need to handle
   303  		// the Strict case.
   304  		rendered[filename] = strings.ReplaceAll(buf.String(), "<no value>", "")
   305  	}
   306  
   307  	return rendered, nil
   308  }
   309  
   310  func cleanupParseError(filename string, err error) error {
   311  	tokens := strings.Split(err.Error(), ": ")
   312  	if len(tokens) == 1 {
   313  		// This might happen if a non-templating error occurs
   314  		return fmt.Errorf("parse error in (%s): %s", filename, err)
   315  	}
   316  	// The first token is "template"
   317  	// The second token is either "filename:lineno" or "filename:lineNo:columnNo"
   318  	location := tokens[1]
   319  	// The remaining tokens make up a stacktrace-like chain, ending with the relevant error
   320  	errMsg := tokens[len(tokens)-1]
   321  	return fmt.Errorf("parse error at (%s): %s", string(location), errMsg)
   322  }
   323  
   324  func cleanupExecError(filename string, err error) error {
   325  	if _, isExecError := err.(template.ExecError); !isExecError {
   326  		return err
   327  	}
   328  
   329  	tokens := strings.SplitN(err.Error(), ": ", 3)
   330  	if len(tokens) != 3 {
   331  		// This might happen if a non-templating error occurs
   332  		return fmt.Errorf("execution error in (%s): %s", filename, err)
   333  	}
   334  
   335  	// The first token is "template"
   336  	// The second token is either "filename:lineno" or "filename:lineNo:columnNo"
   337  	location := tokens[1]
   338  
   339  	parts := warnRegex.FindStringSubmatch(tokens[2])
   340  	if len(parts) >= 2 {
   341  		return fmt.Errorf("execution error at (%s): %s", string(location), parts[1])
   342  	}
   343  
   344  	return err
   345  }
   346  
   347  func sortTemplates(tpls map[string]renderable) []string {
   348  	keys := make([]string, len(tpls))
   349  	i := 0
   350  	for key := range tpls {
   351  		keys[i] = key
   352  		i++
   353  	}
   354  	sort.Sort(sort.Reverse(byPathLen(keys)))
   355  	return keys
   356  }
   357  
   358  type byPathLen []string
   359  
   360  func (p byPathLen) Len() int      { return len(p) }
   361  func (p byPathLen) Swap(i, j int) { p[j], p[i] = p[i], p[j] }
   362  func (p byPathLen) Less(i, j int) bool {
   363  	a, b := p[i], p[j]
   364  	ca, cb := strings.Count(a, "/"), strings.Count(b, "/")
   365  	if ca == cb {
   366  		return strings.Compare(a, b) == -1
   367  	}
   368  	return ca < cb
   369  }
   370  
   371  // allTemplates returns all templates for a chart and its dependencies.
   372  //
   373  // As it goes, it also prepares the values in a scope-sensitive manner.
   374  func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
   375  	templates := make(map[string]renderable)
   376  	recAllTpls(c, templates, vals)
   377  	return templates
   378  }
   379  
   380  // recAllTpls recurses through the templates in a chart.
   381  //
   382  // As it recurses, it also sets the values to be appropriate for the template
   383  // scope.
   384  func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) map[string]interface{} {
   385  	subCharts := make(map[string]interface{})
   386  	chartMetaData := struct {
   387  		chart.Metadata
   388  		IsRoot bool
   389  	}{*c.Metadata, c.IsRoot()}
   390  
   391  	next := map[string]interface{}{
   392  		"Chart":        chartMetaData,
   393  		"Files":        newFiles(c.Files),
   394  		"Release":      vals["Release"],
   395  		"Capabilities": vals["Capabilities"],
   396  		"Values":       make(chartutil.Values),
   397  		"Subcharts":    subCharts,
   398  	}
   399  
   400  	// If there is a {{.Values.ThisChart}} in the parent metadata,
   401  	// copy that into the {{.Values}} for this template.
   402  	if c.IsRoot() {
   403  		next["Values"] = vals["Values"]
   404  	} else if vs, err := vals.Table("Values." + c.Name()); err == nil {
   405  		next["Values"] = vs
   406  	}
   407  
   408  	for _, child := range c.Dependencies() {
   409  		subCharts[child.Name()] = recAllTpls(child, templates, next)
   410  	}
   411  
   412  	newParentID := c.ChartFullPath()
   413  	for _, t := range c.Templates {
   414  		if t == nil {
   415  			continue
   416  		}
   417  		if !isTemplateValid(c, t.Name) {
   418  			continue
   419  		}
   420  		templates[path.Join(newParentID, t.Name)] = renderable{
   421  			tpl:      string(t.Data),
   422  			vals:     next,
   423  			basePath: path.Join(newParentID, "templates"),
   424  		}
   425  	}
   426  
   427  	return next
   428  }
   429  
   430  // isTemplateValid returns true if the template is valid for the chart type
   431  func isTemplateValid(ch *chart.Chart, templateName string) bool {
   432  	if isLibraryChart(ch) {
   433  		return strings.HasPrefix(filepath.Base(templateName), "_")
   434  	}
   435  	return true
   436  }
   437  
   438  // isLibraryChart returns true if the chart is a library chart
   439  func isLibraryChart(c *chart.Chart) bool {
   440  	return strings.EqualFold(c.Metadata.Type, "library")
   441  }
   442  

View as plain text