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