mirror of https://github.com/helm/helm
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
422 lines
13 KiB
422 lines
13 KiB
/*
|
|
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
|
|
// EnableDNS tells the engine to allow DNS lookups when rendering templates
|
|
EnableDNS bool
|
|
}
|
|
|
|
// New creates a new instance of Engine using the passed in rest config.
|
|
func New(config *rest.Config) Engine {
|
|
return Engine{
|
|
config: 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 + `((?s).*)` + 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)
|
|
}
|
|
|
|
// When DNS lookups are not enabled override the sprig function and return
|
|
// an empty string.
|
|
if !e.EnableDNS {
|
|
funcMap["getHostByName"] = func(name string) string {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// 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{})
|
|
chartMetaData := struct {
|
|
chart.Metadata
|
|
IsRoot bool
|
|
}{*c.Metadata, c.IsRoot()}
|
|
|
|
next := map[string]interface{}{
|
|
"Chart": chartMetaData,
|
|
"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 t == nil {
|
|
continue
|
|
}
|
|
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")
|
|
}
|