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