mirror of https://github.com/helm/helm.git
				
				
				
			
		
			
				
	
	
		
			398 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			398 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
| Copyright The Helm Authors.
 | |
| 
 | |
| Licensed under the Apache License, Version 2.0 (the "License");
 | |
| you may not use this file except in compliance with the License.
 | |
| You may obtain a copy of the License at
 | |
| 
 | |
|     http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
| Unless required by applicable law or agreed to in writing, software
 | |
| distributed under the License is distributed on an "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| See the License for the specific language governing permissions and
 | |
| limitations under the License.
 | |
| */
 | |
| 
 | |
| package engine
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"log"
 | |
| 	"path"
 | |
| 	"path/filepath"
 | |
| 	"regexp"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 	"text/template"
 | |
| 
 | |
| 	"github.com/pkg/errors"
 | |
| 	"k8s.io/client-go/rest"
 | |
| 
 | |
| 	"helm.sh/helm/v3/pkg/chart"
 | |
| 	"helm.sh/helm/v3/pkg/chartutil"
 | |
| )
 | |
| 
 | |
| // Engine is an implementation of the Helm rendering implementation for templates.
 | |
| type Engine struct {
 | |
| 	// If strict is enabled, template rendering will fail if a template references
 | |
| 	// a value that was not passed in.
 | |
| 	Strict bool
 | |
| 	// In LintMode, some 'required' template values may be missing, so don't fail
 | |
| 	LintMode bool
 | |
| 	// the rest config to connect to the kubernetes api
 | |
| 	config *rest.Config
 | |
| }
 | |
| 
 | |
| // Render takes a chart, optional values, and value overrides, and attempts to render the Go templates.
 | |
| //
 | |
| // Render can be called repeatedly on the same engine.
 | |
| //
 | |
| // This will look in the chart's 'templates' data (e.g. the 'templates/' directory)
 | |
| // and attempt to render the templates there using the values passed in.
 | |
| //
 | |
| // Values are scoped to their templates. A dependency template will not have
 | |
| // access to the values set for its parent. If chart "foo" includes chart "bar",
 | |
| // "bar" will not have access to the values for "foo".
 | |
| //
 | |
| // Values should be prepared with something like `chartutils.ReadValues`.
 | |
| //
 | |
| // Values are passed through the templates according to scope. If the top layer
 | |
| // chart includes the chart foo, which includes the chart bar, the values map
 | |
| // will be examined for a table called "foo". If "foo" is found in vals,
 | |
| // that section of the values will be passed into the "foo" chart. And if that
 | |
| // section contains a value named "bar", that value will be passed on to the
 | |
| // bar chart during render time.
 | |
| func (e Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
 | |
| 	tmap := allTemplates(chrt, values)
 | |
| 	return e.render(tmap)
 | |
| }
 | |
| 
 | |
| // Render takes a chart, optional values, and value overrides, and attempts to
 | |
| // render the Go templates using the default options.
 | |
| func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
 | |
| 	return new(Engine).Render(chrt, values)
 | |
| }
 | |
| 
 | |
| // RenderWithClient takes a chart, optional values, and value overrides, and attempts to
 | |
| // render the Go templates using the default options. This engine is client aware and so can have template
 | |
| // functions that interact with the client
 | |
| func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config) (map[string]string, error) {
 | |
| 	return Engine{
 | |
| 		config: config,
 | |
| 	}.Render(chrt, values)
 | |
| }
 | |
| 
 | |
| // renderable is an object that can be rendered.
 | |
| type renderable struct {
 | |
| 	// tpl is the current template.
 | |
| 	tpl string
 | |
| 	// vals are the values to be supplied to the template.
 | |
| 	vals chartutil.Values
 | |
| 	// namespace prefix to the templates of the current chart
 | |
| 	basePath string
 | |
| }
 | |
| 
 | |
| const warnStartDelim = "HELM_ERR_START"
 | |
| const warnEndDelim = "HELM_ERR_END"
 | |
| const recursionMaxNums = 1000
 | |
| 
 | |
| var warnRegex = regexp.MustCompile(warnStartDelim + `(.*)` + warnEndDelim)
 | |
| 
 | |
| func warnWrap(warn string) string {
 | |
| 	return warnStartDelim + warn + warnEndDelim
 | |
| }
 | |
| 
 | |
| // initFunMap creates the Engine's FuncMap and adds context-specific functions.
 | |
| func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]renderable) {
 | |
| 	funcMap := funcMap()
 | |
| 	includedNames := make(map[string]int)
 | |
| 
 | |
| 	// Add the 'include' function here so we can close over t.
 | |
| 	funcMap["include"] = func(name string, data interface{}) (string, error) {
 | |
| 		var buf strings.Builder
 | |
| 		if v, ok := includedNames[name]; ok {
 | |
| 			if v > recursionMaxNums {
 | |
| 				return "", errors.Wrapf(fmt.Errorf("unable to execute template"), "rendering template has a nested reference name: %s", name)
 | |
| 			}
 | |
| 			includedNames[name]++
 | |
| 		} else {
 | |
| 			includedNames[name] = 1
 | |
| 		}
 | |
| 		err := t.ExecuteTemplate(&buf, name, data)
 | |
| 		includedNames[name]--
 | |
| 		return buf.String(), err
 | |
| 	}
 | |
| 
 | |
| 	// Add the 'tpl' function here
 | |
| 	funcMap["tpl"] = func(tpl string, vals chartutil.Values) (string, error) {
 | |
| 		basePath, err := vals.PathValue("Template.BasePath")
 | |
| 		if err != nil {
 | |
| 			return "", errors.Wrapf(err, "cannot retrieve Template.Basepath from values inside tpl function: %s", tpl)
 | |
| 		}
 | |
| 
 | |
| 		templateName, err := vals.PathValue("Template.Name")
 | |
| 		if err != nil {
 | |
| 			return "", errors.Wrapf(err, "cannot retrieve Template.Name from values inside tpl function: %s", tpl)
 | |
| 		}
 | |
| 
 | |
| 		templates := map[string]renderable{
 | |
| 			templateName.(string): {
 | |
| 				tpl:      tpl,
 | |
| 				vals:     vals,
 | |
| 				basePath: basePath.(string),
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		result, err := e.renderWithReferences(templates, referenceTpls)
 | |
| 		if err != nil {
 | |
| 			return "", errors.Wrapf(err, "error during tpl function execution for %q", tpl)
 | |
| 		}
 | |
| 		return result[templateName.(string)], nil
 | |
| 	}
 | |
| 
 | |
| 	// Add the `required` function here so we can use lintMode
 | |
| 	funcMap["required"] = func(warn string, val interface{}) (interface{}, error) {
 | |
| 		if val == nil {
 | |
| 			if e.LintMode {
 | |
| 				// Don't fail on missing required values when linting
 | |
| 				log.Printf("[INFO] Missing required value: %s", warn)
 | |
| 				return "", nil
 | |
| 			}
 | |
| 			return val, errors.Errorf(warnWrap(warn))
 | |
| 		} else if _, ok := val.(string); ok {
 | |
| 			if val == "" {
 | |
| 				if e.LintMode {
 | |
| 					// Don't fail on missing required values when linting
 | |
| 					log.Printf("[INFO] Missing required value: %s", warn)
 | |
| 					return "", nil
 | |
| 				}
 | |
| 				return val, errors.Errorf(warnWrap(warn))
 | |
| 			}
 | |
| 		}
 | |
| 		return val, nil
 | |
| 	}
 | |
| 
 | |
| 	// Override sprig fail function for linting and wrapping message
 | |
| 	funcMap["fail"] = func(msg string) (string, error) {
 | |
| 		if e.LintMode {
 | |
| 			// Don't fail when linting
 | |
| 			log.Printf("[INFO] Fail: %s", msg)
 | |
| 			return "", nil
 | |
| 		}
 | |
| 		return "", errors.New(warnWrap(msg))
 | |
| 	}
 | |
| 
 | |
| 	// If we are not linting and have a cluster connection, provide a Kubernetes-backed
 | |
| 	// implementation.
 | |
| 	if !e.LintMode && e.config != nil {
 | |
| 		funcMap["lookup"] = NewLookupFunction(e.config)
 | |
| 	}
 | |
| 
 | |
| 	t.Funcs(funcMap)
 | |
| }
 | |
| 
 | |
| // render takes a map of templates/values and renders them.
 | |
| func (e Engine) render(tpls map[string]renderable) (map[string]string, error) {
 | |
| 	return e.renderWithReferences(tpls, tpls)
 | |
| }
 | |
| 
 | |
| // renderWithReferences takes a map of templates/values to render, and a map of
 | |
| // templates which can be referenced within them.
 | |
| func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable) (rendered map[string]string, err error) {
 | |
| 	// Basically, what we do here is start with an empty parent template and then
 | |
| 	// build up a list of templates -- one for each file. Once all of the templates
 | |
| 	// have been parsed, we loop through again and execute every template.
 | |
| 	//
 | |
| 	// The idea with this process is to make it possible for more complex templates
 | |
| 	// to share common blocks, but to make the entire thing feel like a file-based
 | |
| 	// template engine.
 | |
| 	defer func() {
 | |
| 		if r := recover(); r != nil {
 | |
| 			err = errors.Errorf("rendering template failed: %v", r)
 | |
| 		}
 | |
| 	}()
 | |
| 	t := template.New("gotpl")
 | |
| 	if e.Strict {
 | |
| 		t.Option("missingkey=error")
 | |
| 	} else {
 | |
| 		// Not that zero will attempt to add default values for types it knows,
 | |
| 		// but will still emit <no value> for others. We mitigate that later.
 | |
| 		t.Option("missingkey=zero")
 | |
| 	}
 | |
| 
 | |
| 	e.initFunMap(t, referenceTpls)
 | |
| 
 | |
| 	// We want to parse the templates in a predictable order. The order favors
 | |
| 	// higher-level (in file system) templates over deeply nested templates.
 | |
| 	keys := sortTemplates(tpls)
 | |
| 	referenceKeys := sortTemplates(referenceTpls)
 | |
| 
 | |
| 	for _, filename := range keys {
 | |
| 		r := tpls[filename]
 | |
| 		if _, err := t.New(filename).Parse(r.tpl); err != nil {
 | |
| 			return map[string]string{}, cleanupParseError(filename, err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Adding the reference templates to the template context
 | |
| 	// so they can be referenced in the tpl function
 | |
| 	for _, filename := range referenceKeys {
 | |
| 		if t.Lookup(filename) == nil {
 | |
| 			r := referenceTpls[filename]
 | |
| 			if _, err := t.New(filename).Parse(r.tpl); err != nil {
 | |
| 				return map[string]string{}, cleanupParseError(filename, err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	rendered = make(map[string]string, len(keys))
 | |
| 	for _, filename := range keys {
 | |
| 		// Don't render partials. We don't care out the direct output of partials.
 | |
| 		// They are only included from other templates.
 | |
| 		if strings.HasPrefix(path.Base(filename), "_") {
 | |
| 			continue
 | |
| 		}
 | |
| 		// At render time, add information about the template that is being rendered.
 | |
| 		vals := tpls[filename].vals
 | |
| 		vals["Template"] = chartutil.Values{"Name": filename, "BasePath": tpls[filename].basePath}
 | |
| 		var buf strings.Builder
 | |
| 		if err := t.ExecuteTemplate(&buf, filename, vals); err != nil {
 | |
| 			return map[string]string{}, cleanupExecError(filename, err)
 | |
| 		}
 | |
| 		delete(vals, "Template")
 | |
| 
 | |
| 		// Work around the issue where Go will emit "<no value>" even if Options(missing=zero)
 | |
| 		// is set. Since missing=error will never get here, we do not need to handle
 | |
| 		// the Strict case.
 | |
| 		rendered[filename] = strings.ReplaceAll(buf.String(), "<no value>", "")
 | |
| 	}
 | |
| 
 | |
| 	return rendered, nil
 | |
| }
 | |
| 
 | |
| func cleanupParseError(filename string, err error) error {
 | |
| 	tokens := strings.Split(err.Error(), ": ")
 | |
| 	if len(tokens) == 1 {
 | |
| 		// This might happen if a non-templating error occurs
 | |
| 		return fmt.Errorf("parse error in (%s): %s", filename, err)
 | |
| 	}
 | |
| 	// The first token is "template"
 | |
| 	// The second token is either "filename:lineno" or "filename:lineNo:columnNo"
 | |
| 	location := tokens[1]
 | |
| 	// The remaining tokens make up a stacktrace-like chain, ending with the relevant error
 | |
| 	errMsg := tokens[len(tokens)-1]
 | |
| 	return fmt.Errorf("parse error at (%s): %s", string(location), errMsg)
 | |
| }
 | |
| 
 | |
| func cleanupExecError(filename string, err error) error {
 | |
| 	if _, isExecError := err.(template.ExecError); !isExecError {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	tokens := strings.SplitN(err.Error(), ": ", 3)
 | |
| 	if len(tokens) != 3 {
 | |
| 		// This might happen if a non-templating error occurs
 | |
| 		return fmt.Errorf("execution error in (%s): %s", filename, err)
 | |
| 	}
 | |
| 
 | |
| 	// The first token is "template"
 | |
| 	// The second token is either "filename:lineno" or "filename:lineNo:columnNo"
 | |
| 	location := tokens[1]
 | |
| 
 | |
| 	parts := warnRegex.FindStringSubmatch(tokens[2])
 | |
| 	if len(parts) >= 2 {
 | |
| 		return fmt.Errorf("execution error at (%s): %s", string(location), parts[1])
 | |
| 	}
 | |
| 
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func sortTemplates(tpls map[string]renderable) []string {
 | |
| 	keys := make([]string, len(tpls))
 | |
| 	i := 0
 | |
| 	for key := range tpls {
 | |
| 		keys[i] = key
 | |
| 		i++
 | |
| 	}
 | |
| 	sort.Sort(sort.Reverse(byPathLen(keys)))
 | |
| 	return keys
 | |
| }
 | |
| 
 | |
| type byPathLen []string
 | |
| 
 | |
| func (p byPathLen) Len() int      { return len(p) }
 | |
| func (p byPathLen) Swap(i, j int) { p[j], p[i] = p[i], p[j] }
 | |
| func (p byPathLen) Less(i, j int) bool {
 | |
| 	a, b := p[i], p[j]
 | |
| 	ca, cb := strings.Count(a, "/"), strings.Count(b, "/")
 | |
| 	if ca == cb {
 | |
| 		return strings.Compare(a, b) == -1
 | |
| 	}
 | |
| 	return ca < cb
 | |
| }
 | |
| 
 | |
| // allTemplates returns all templates for a chart and its dependencies.
 | |
| //
 | |
| // As it goes, it also prepares the values in a scope-sensitive manner.
 | |
| func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
 | |
| 	templates := make(map[string]renderable)
 | |
| 	recAllTpls(c, templates, vals)
 | |
| 	return templates
 | |
| }
 | |
| 
 | |
| // recAllTpls recurses through the templates in a chart.
 | |
| //
 | |
| // As it recurses, it also sets the values to be appropriate for the template
 | |
| // scope.
 | |
| func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) map[string]interface{} {
 | |
| 	subCharts := make(map[string]interface{})
 | |
| 	next := map[string]interface{}{
 | |
| 		"Chart":        c.Metadata,
 | |
| 		"Files":        newFiles(c.Files),
 | |
| 		"Release":      vals["Release"],
 | |
| 		"Capabilities": vals["Capabilities"],
 | |
| 		"Values":       make(chartutil.Values),
 | |
| 		"Subcharts":    subCharts,
 | |
| 	}
 | |
| 
 | |
| 	// If there is a {{.Values.ThisChart}} in the parent metadata,
 | |
| 	// copy that into the {{.Values}} for this template.
 | |
| 	if c.IsRoot() {
 | |
| 		next["Values"] = vals["Values"]
 | |
| 	} else if vs, err := vals.Table("Values." + c.Name()); err == nil {
 | |
| 		next["Values"] = vs
 | |
| 	}
 | |
| 
 | |
| 	for _, child := range c.Dependencies() {
 | |
| 		subCharts[child.Name()] = recAllTpls(child, templates, next)
 | |
| 	}
 | |
| 
 | |
| 	newParentID := c.ChartFullPath()
 | |
| 	for _, t := range c.Templates {
 | |
| 		if !isTemplateValid(c, t.Name) {
 | |
| 			continue
 | |
| 		}
 | |
| 		templates[path.Join(newParentID, t.Name)] = renderable{
 | |
| 			tpl:      string(t.Data),
 | |
| 			vals:     next,
 | |
| 			basePath: path.Join(newParentID, "templates"),
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return next
 | |
| }
 | |
| 
 | |
| // isTemplateValid returns true if the template is valid for the chart type
 | |
| func isTemplateValid(ch *chart.Chart, templateName string) bool {
 | |
| 	if isLibraryChart(ch) {
 | |
| 		return strings.HasPrefix(filepath.Base(templateName), "_")
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // isLibraryChart returns true if the chart is a library chart
 | |
| func isLibraryChart(c *chart.Chart) bool {
 | |
| 	return strings.EqualFold(c.Metadata.Type, "library")
 | |
| }
 |