ref(pkg/engine): make template specific functions private

Make template specific functions private to ensure they not misused and
make unit tests simpler.  We may export the template helpers later if
needed.

This lays the foundation for the new chart pipeline.

Signed-off-by: Adam Reese <adam@reese.io>
pull/5412/head
Adam Reese 6 years ago
parent d841a1b1d9
commit 849f27d11f
No known key found for this signature in database
GPG Key ID: 06F35E60A7A18DD6

@ -175,9 +175,6 @@ func (o *templateOptions) run(out io.Writer) error {
return err return err
} }
// Set up engine.
renderer := engine.New()
// kubernetes version // kubernetes version
kv, err := semver.NewVersion(o.kubeVersion) kv, err := semver.NewVersion(o.kubeVersion)
if err != nil { if err != nil {
@ -194,7 +191,7 @@ func (o *templateOptions) run(out io.Writer) error {
return err return err
} }
rendered, err := renderer.Render(c, vals) rendered, err := engine.Render(c, vals)
if err != nil { if err != nil {
return err return err
} }

@ -268,7 +268,7 @@ func (i *Install) renderResources(ch *chart.Chart, values chartutil.Values, vs c
} }
} }
files, err := engine.New().Render(ch, values) files, err := engine.Render(ch, values)
if err != nil { if err != nil {
return hooks, buf, "", err return hooks, buf, "", err
} }

@ -218,17 +218,17 @@ func TestProcessDependencyImportValues(t *testing.T) {
t.Fatalf("retrieving import values table %v %v", kk, err) t.Fatalf("retrieving import values table %v %v", kk, err)
} }
switch pv.(type) { switch pv := pv.(type) {
case float64: case float64:
if s := strconv.FormatFloat(pv.(float64), 'f', -1, 64); s != vv { if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv {
t.Errorf("failed to match imported float value %v with expected %v", s, vv) t.Errorf("failed to match imported float value %v with expected %v", s, vv)
} }
case bool: case bool:
if b := strconv.FormatBool(pv.(bool)); b != vv { if b := strconv.FormatBool(pv); b != vv {
t.Errorf("failed to match imported bool value %v with expected %v", b, vv) t.Errorf("failed to match imported bool value %v with expected %v", b, vv)
} }
default: default:
if pv.(string) != vv { if pv != vv {
t.Errorf("failed to match imported string value %q with expected %q", pv, vv) t.Errorf("failed to match imported string value %q with expected %q", pv, vv)
} }
} }

@ -286,11 +286,12 @@ func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} {
// values. // values.
for key, val := range src { for key, val := range src {
if istable(val) { if istable(val) {
if innerdst, ok := dst[key]; !ok { switch innerdst, ok := dst[key]; {
case !ok:
dst[key] = val dst[key] = val
} else if istable(innerdst) { case istable(innerdst):
CoalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{})) CoalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{}))
} else { default:
log.Printf("warning: cannot overwrite table with non table for %s (%v)", key, val) log.Printf("warning: cannot overwrite table with non table for %s (%v)", key, val)
} }
} else if dv, ok := dst[key]; ok && istable(dv) { } else if dv, ok := dst[key]; ok && istable(dv) {
@ -316,15 +317,14 @@ type ReleaseOptions struct {
func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities) (Values, error) { func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities) (Values, error) {
top := map[string]interface{}{ top := map[string]interface{}{
"Chart": chrt.Metadata,
"Capabilities": caps,
"Release": map[string]interface{}{ "Release": map[string]interface{}{
"Name": options.Name, "Name": options.Name,
"IsUpgrade": options.IsUpgrade, "IsUpgrade": options.IsUpgrade,
"IsInstall": options.IsInstall, "IsInstall": options.IsInstall,
"Service": "Helm", "Service": "Helm",
}, },
"Chart": chrt.Metadata,
"Files": NewFiles(chrt.Files),
"Capabilities": caps,
} }
vals, err := CoalesceValues(chrt, chrtVals) vals, err := CoalesceValues(chrt, chrtVals)

@ -128,9 +128,6 @@ func TestToRenderValues(t *testing.T) {
if !relmap["IsInstall"].(bool) { if !relmap["IsInstall"].(bool) {
t.Errorf("Expected install to be true.") t.Errorf("Expected install to be true.")
} }
if data := res["Files"].(Files)["scheherazade/shahryar.txt"]; string(data) != "1,001 Nights" {
t.Errorf("Expected file '1,001 Nights', got %q", string(data))
}
if !res["Capabilities"].(*Capabilities).APIVersions.Has("v1") { if !res["Capabilities"].(*Capabilities).APIVersions.Has("v1") {
t.Error("Expected Capabilities to have v1 as an API") t.Error("Expected Capabilities to have v1 as an API")
} }

@ -22,7 +22,6 @@ import (
"strings" "strings"
"text/template" "text/template"
"github.com/Masterminds/sprig"
"github.com/pkg/errors" "github.com/pkg/errors"
"k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/chart"
@ -31,67 +30,11 @@ import (
// Engine is an implementation of 'cmd/tiller/environment'.Engine that uses Go templates. // Engine is an implementation of 'cmd/tiller/environment'.Engine that uses Go templates.
type Engine struct { type Engine struct {
// FuncMap contains the template functions that will be passed to each
// render call. This may only be modified before the first call to Render.
funcMap template.FuncMap
// If strict is enabled, template rendering will fail if a template references // If strict is enabled, template rendering will fail if a template references
// a value that was not passed in. // a value that was not passed in.
Strict bool Strict bool
} }
// New creates a new Go template Engine instance.
//
// The FuncMap is initialized here. You may modify the FuncMap _prior to_ the
// first invocation of Render.
//
// The FuncMap sets all of the Sprig functions except for those that provide
// access to the underlying OS (env, expandenv).
func New() *Engine {
return &Engine{funcMap: FuncMap()}
}
// FuncMap returns a mapping of all of the functions that Engine has.
//
// Because some functions are late-bound (e.g. contain context-sensitive
// data), the functions may not all perform identically outside of an
// Engine as they will inside of an Engine.
//
// Known late-bound functions:
//
// - "include": This is late-bound in Engine.Render(). The version
// included in the FuncMap is a placeholder.
// - "required": This is late-bound in Engine.Render(). The version
// included in the FuncMap is a placeholder.
// - "tpl": This is late-bound in Engine.Render(). The version
// included in the FuncMap is a placeholder.
func FuncMap() template.FuncMap {
f := sprig.TxtFuncMap()
delete(f, "env")
delete(f, "expandenv")
// Add some extra functionality
extra := template.FuncMap{
"toToml": chartutil.ToTOML,
"toYaml": chartutil.ToYAML,
"fromYaml": chartutil.FromYAML,
"toJson": chartutil.ToJSON,
"fromJson": chartutil.FromJSON,
// This is a placeholder for the "include" function, which is
// late-bound to a template. By declaring it here, we preserve the
// integrity of the linter.
"include": func(string, interface{}) string { return "not implemented" },
"required": func(string, interface{}) interface{} { return "not implemented" },
"tpl": func(string, interface{}) interface{} { return "not implemented" },
}
for k, v := range extra {
f[k] = v
}
return f
}
// Render takes a chart, optional values, and value overrides, and attempts to render the Go templates. // 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. // Render can be called repeatedly on the same engine.
@ -111,12 +54,17 @@ func FuncMap() template.FuncMap {
// that section of the values will be passed into the "foo" chart. And if that // 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 // section contains a value named "bar", that value will be passed on to the
// bar chart during render time. // bar chart during render time.
func (e *Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) { func (e Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
// Render the charts
tmap := allTemplates(chrt, values) tmap := allTemplates(chrt, values)
return e.render(tmap) 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)
}
// renderable is an object that can be rendered. // renderable is an object that can be rendered.
type renderable struct { type renderable struct {
// tpl is the current template. // tpl is the current template.
@ -127,15 +75,9 @@ type renderable struct {
basePath string basePath string
} }
// alterFuncMap takes the Engine's FuncMap and adds context-specific functions. // initFunMap creates the Engine's FuncMap and adds context-specific functions.
// func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]renderable) {
// The resulting FuncMap is only valid for the passed-in template. funcMap := funcMap()
func (e *Engine) alterFuncMap(t *template.Template, referenceTpls map[string]renderable) template.FuncMap {
// Clone the func map because we are adding context-specific functions.
funcMap := make(template.FuncMap)
for k, v := range e.funcMap {
funcMap[k] = v
}
// Add the 'include' function here so we can close over t. // Add the 'include' function here so we can close over t.
funcMap["include"] = func(name string, data interface{}) (string, error) { funcMap["include"] = func(name string, data interface{}) (string, error) {
@ -144,18 +86,6 @@ func (e *Engine) alterFuncMap(t *template.Template, referenceTpls map[string]ren
return buf.String(), err return buf.String(), err
} }
// Add the 'required' function here
funcMap["required"] = func(warn string, val interface{}) (interface{}, error) {
if val == nil {
return val, errors.Errorf(warn)
} else if _, ok := val.(string); ok {
if val == "" {
return val, errors.Errorf(warn)
}
}
return val, nil
}
// Add the 'tpl' function here // Add the 'tpl' function here
funcMap["tpl"] = func(tpl string, vals chartutil.Values) (string, error) { funcMap["tpl"] = func(tpl string, vals chartutil.Values) (string, error) {
basePath, err := vals.PathValue("Template.BasePath") basePath, err := vals.PathValue("Template.BasePath")
@ -163,19 +93,18 @@ func (e *Engine) alterFuncMap(t *template.Template, referenceTpls map[string]ren
return "", errors.Wrapf(err, "cannot retrieve Template.Basepath from values inside tpl function: %s", tpl) return "", errors.Wrapf(err, "cannot retrieve Template.Basepath from values inside tpl function: %s", tpl)
} }
r := renderable{
tpl: tpl,
vals: vals,
basePath: basePath.(string),
}
templateName, err := vals.PathValue("Template.Name") templateName, err := vals.PathValue("Template.Name")
if err != nil { if err != nil {
return "", errors.Wrapf(err, "cannot retrieve Template.Name from values inside tpl function: %s", tpl) return "", errors.Wrapf(err, "cannot retrieve Template.Name from values inside tpl function: %s", tpl)
} }
templates := make(map[string]renderable) templates := map[string]renderable{
templates[templateName.(string)] = r templateName.(string): {
tpl: tpl,
vals: vals,
basePath: basePath.(string),
},
}
result, err := e.renderWithReferences(templates, referenceTpls) result, err := e.renderWithReferences(templates, referenceTpls)
if err != nil { if err != nil {
@ -183,19 +112,17 @@ func (e *Engine) alterFuncMap(t *template.Template, referenceTpls map[string]ren
} }
return result[templateName.(string)], nil return result[templateName.(string)], nil
} }
t.Funcs(funcMap)
return funcMap
} }
// render takes a map of templates/values and renders them. // render takes a map of templates/values and renders them.
func (e *Engine) render(tpls map[string]renderable) (rendered map[string]string, err error) { func (e Engine) render(tpls map[string]renderable) (map[string]string, error) {
return e.renderWithReferences(tpls, tpls) return e.renderWithReferences(tpls, tpls)
} }
// renderWithReferences takes a map of templates/values to render, and a map of // renderWithReferences takes a map of templates/values to render, and a map of
// templates which can be referenced within them. // templates which can be referenced within them.
func (e *Engine) renderWithReferences(tpls, referenceTpls map[string]renderable) (rendered map[string]string, err error) { 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 // 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 // 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. // have been parsed, we loop through again and execute every template.
@ -217,34 +144,31 @@ func (e *Engine) renderWithReferences(tpls, referenceTpls map[string]renderable)
t.Option("missingkey=zero") t.Option("missingkey=zero")
} }
funcMap := e.alterFuncMap(t, referenceTpls) e.initFunMap(t, referenceTpls)
// We want to parse the templates in a predictable order. The order favors // We want to parse the templates in a predictable order. The order favors
// higher-level (in file system) templates over deeply nested templates. // higher-level (in file system) templates over deeply nested templates.
keys := sortTemplates(tpls) keys := sortTemplates(tpls)
files := []string{}
for _, fname := range keys { for _, fname := range keys {
r := tpls[fname] r := tpls[fname]
if _, err := t.New(fname).Funcs(funcMap).Parse(r.tpl); err != nil { if _, err := t.New(fname).Parse(r.tpl); err != nil {
return map[string]string{}, errors.Wrapf(err, "parse error in %q", fname) return map[string]string{}, errors.Wrapf(err, "parse error in %q", fname)
} }
files = append(files, fname)
} }
// Adding the reference templates to the template context // Adding the reference templates to the template context
// so they can be referenced in the tpl function // so they can be referenced in the tpl function
for fname, r := range referenceTpls { for fname, r := range referenceTpls {
if t.Lookup(fname) == nil { if t.Lookup(fname) == nil {
if _, err := t.New(fname).Funcs(funcMap).Parse(r.tpl); err != nil { if _, err := t.New(fname).Parse(r.tpl); err != nil {
return map[string]string{}, errors.Wrapf(err, "parse error in %q", fname) return map[string]string{}, errors.Wrapf(err, "parse error in %q", fname)
} }
} }
} }
rendered = make(map[string]string, len(files)) rendered = make(map[string]string, len(keys))
for _, file := range files { for _, file := range keys {
// Don't render partials. We don't care out the direct output of partials. // Don't render partials. We don't care out the direct output of partials.
// They are only included from other templates. // They are only included from other templates.
if strings.HasPrefix(path.Base(file), "_") { if strings.HasPrefix(path.Base(file), "_") {
@ -266,9 +190,6 @@ func (e *Engine) renderWithReferences(tpls, referenceTpls map[string]renderable)
Data: []byte(strings.Replace(buf.String(), "<no value>", "", -1)), Data: []byte(strings.Replace(buf.String(), "<no value>", "", -1)),
} }
rendered[file] = string(f.Data) rendered[file] = string(f.Data)
// if ch != nil {
// ch.Files = append(ch.Files, f)
// }
} }
return rendered, nil return rendered, nil
@ -311,36 +232,32 @@ func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
// //
// As it recurses, it also sets the values to be appropriate for the template // As it recurses, it also sets the values to be appropriate for the template
// scope. // scope.
func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals chartutil.Values) { func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) {
// This should never evaluate to a nil map. That will cause problems when next := map[string]interface{}{
// values are appended later. "Chart": c.Metadata,
cvals := make(chartutil.Values) "Files": newFiles(c.Files),
"Release": vals["Release"],
"Capabilities": vals["Capabilities"],
"Values": make(chartutil.Values),
}
// If there is a {{.Values.ThisChart}} in the parent metadata,
// copy that into the {{.Values}} for this template.
if c.IsRoot() { if c.IsRoot() {
cvals = parentVals next["Values"] = vals["Values"]
} else if c.Name() != "" { } else if vs, err := vals.Table("Values." + c.Name()); err == nil {
cvals = map[string]interface{}{ next["Values"] = vs
"Values": make(chartutil.Values),
"Release": parentVals["Release"],
"Chart": c.Metadata,
"Files": chartutil.NewFiles(c.Files),
"Capabilities": parentVals["Capabilities"],
}
// If there is a {{.Values.ThisChart}} in the parent metadata,
// copy that into the {{.Values}} for this template.
if vs, err := parentVals.Table("Values." + c.Name()); err == nil {
cvals["Values"] = vs
}
} }
for _, child := range c.Dependencies() { for _, child := range c.Dependencies() {
recAllTpls(child, templates, cvals) recAllTpls(child, templates, next)
} }
newParentID := c.ChartFullPath() newParentID := c.ChartFullPath()
for _, t := range c.Templates { for _, t := range c.Templates {
templates[path.Join(newParentID, t.Name)] = renderable{ templates[path.Join(newParentID, t.Name)] = renderable{
tpl: string(t.Data), tpl: string(t.Data),
vals: cvals, vals: next,
basePath: path.Join(newParentID, "templates"), basePath: path.Join(newParentID, "templates"),
} }
} }

@ -18,6 +18,7 @@ package engine
import ( import (
"fmt" "fmt"
"strings"
"sync" "sync"
"testing" "testing"
@ -51,25 +52,16 @@ func TestSortTemplates(t *testing.T) {
} }
for i, e := range expect { for i, e := range expect {
if got[i] != e { if got[i] != e {
t.Errorf("expected %q, got %q at index %d\n\tExp: %#v\n\tGot: %#v", e, got[i], i, expect, got) t.Fatalf("\n\tExp:\n%s\n\tGot:\n%s",
} strings.Join(expect, "\n"),
} strings.Join(got, "\n"),
} )
func TestEngine(t *testing.T) {
e := New()
// Forbidden because they allow access to the host OS.
forbidden := []string{"env", "expandenv"}
for _, f := range forbidden {
if _, ok := e.funcMap[f]; ok {
t.Errorf("Forbidden function %s exists in FuncMap.", f)
} }
} }
} }
func TestFuncMap(t *testing.T) { func TestFuncMap(t *testing.T) {
fns := FuncMap() fns := funcMap()
forbidden := []string{"env", "expandenv"} forbidden := []string{"env", "expandenv"}
for _, f := range forbidden { for _, f := range forbidden {
if _, ok := fns[f]; ok { if _, ok := fns[f]; ok {
@ -93,53 +85,49 @@ func TestRender(t *testing.T) {
Version: "1.2.3", Version: "1.2.3",
}, },
Templates: []*chart.File{ Templates: []*chart.File{
{Name: "templates/test1", Data: []byte("{{.outer | title }} {{.inner | title}}")}, {Name: "templates/test1", Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")},
{Name: "templates/test2", Data: []byte("{{.global.callme | lower }}")}, {Name: "templates/test2", Data: []byte("{{.Values.global.callme | lower }}")},
{Name: "templates/test3", Data: []byte("{{.noValue}}")}, {Name: "templates/test3", Data: []byte("{{.noValue}}")},
{Name: "templates/test4", Data: []byte("{{toJson .Values}}")},
}, },
Values: map[string]interface{}{"outer": "DEFAULT", "inner": "DEFAULT"}, Values: map[string]interface{}{"outer": "DEFAULT", "inner": "DEFAULT"},
} }
vals := map[string]interface{}{ vals := map[string]interface{}{
"outer": "spouter", "Values": map[string]interface{}{
"inner": "inn", "outer": "spouter",
"global": map[string]interface{}{ "inner": "inn",
"callme": "Ishmael", "global": map[string]interface{}{
"callme": "Ishmael",
},
}, },
} }
e := New()
v, err := chartutil.CoalesceValues(c, vals) v, err := chartutil.CoalesceValues(c, vals)
if err != nil { if err != nil {
t.Fatalf("Failed to coalesce values: %s", err) t.Fatalf("Failed to coalesce values: %s", err)
} }
out, err := e.Render(c, v) out, err := Render(c, v)
if err != nil { if err != nil {
t.Errorf("Failed to render templates: %s", err) t.Errorf("Failed to render templates: %s", err)
} }
expect := "Spouter Inn" expect := map[string]string{
if out["moby/templates/test1"] != expect { "moby/templates/test1": "Spouter Inn",
t.Errorf("Expected %q, got %q", expect, out["test1"]) "moby/templates/test2": "ishmael",
} "moby/templates/test3": "",
"moby/templates/test4": `{"global":{"callme":"Ishmael"},"inner":"inn","outer":"spouter"}`,
expect = "ishmael"
if out["moby/templates/test2"] != expect {
t.Errorf("Expected %q, got %q", expect, out["test2"])
}
expect = ""
if out["moby/templates/test3"] != expect {
t.Errorf("Expected %q, got %q", expect, out["test3"])
} }
if _, err := e.Render(c, v); err != nil { for name, data := range expect {
t.Errorf("Unexpected error: %s", err) if out[name] != data {
t.Errorf("Expected %q, got %q", data, out[name])
}
} }
} }
func TestRenderInternals(t *testing.T) { func TestRenderInternals(t *testing.T) {
// Test the internals of the rendering tool. // Test the internals of the rendering tool.
e := New()
vals := chartutil.Values{"Name": "one", "Value": "two"} vals := chartutil.Values{"Name": "one", "Value": "two"}
tpls := map[string]renderable{ tpls := map[string]renderable{
@ -150,7 +138,7 @@ func TestRenderInternals(t *testing.T) {
"three": {tpl: `{{template "two" dict "Value" "three"}}`, vals: vals}, "three": {tpl: `{{template "two" dict "Value" "three"}}`, vals: vals},
} }
out, err := e.render(tpls) out, err := new(Engine).render(tpls)
if err != nil { if err != nil {
t.Fatalf("Failed template rendering: %s", err) t.Fatalf("Failed template rendering: %s", err)
} }
@ -174,21 +162,24 @@ func TestRenderInternals(t *testing.T) {
func TestParallelRenderInternals(t *testing.T) { func TestParallelRenderInternals(t *testing.T) {
// Make sure that we can use one Engine to run parallel template renders. // Make sure that we can use one Engine to run parallel template renders.
e := New() e := new(Engine)
var wg sync.WaitGroup var wg sync.WaitGroup
for i := 0; i < 20; i++ { for i := 0; i < 20; i++ {
wg.Add(1) wg.Add(1)
go func(i int) { go func(i int) {
fname := "my/file/name"
tt := fmt.Sprintf("expect-%d", i) tt := fmt.Sprintf("expect-%d", i)
v := chartutil.Values{"val": tt} tpls := map[string]renderable{
tpls := map[string]renderable{fname: {tpl: `{{.val}}`, vals: v}} "t": {
tpl: `{{.val}}`,
vals: map[string]interface{}{"val": tt},
},
}
out, err := e.render(tpls) out, err := e.render(tpls)
if err != nil { if err != nil {
t.Errorf("Failed to render %s: %s", tt, err) t.Errorf("Failed to render %s: %s", tt, err)
} }
if out[fname] != tt { if out["t"] != tt {
t.Errorf("Expected %q, got %q", tt, out[fname]) t.Errorf("Expected %q, got %q", tt, out["t"])
} }
wg.Done() wg.Done()
}(i) }(i)
@ -221,15 +212,13 @@ func TestAllTemplates(t *testing.T) {
} }
dep1.AddDependency(dep2) dep1.AddDependency(dep2)
var v chartutil.Values tpls := allTemplates(ch1, chartutil.Values{})
tpls := allTemplates(ch1, v)
if len(tpls) != 5 { if len(tpls) != 5 {
t.Errorf("Expected 5 charts, got %d", len(tpls)) t.Errorf("Expected 5 charts, got %d", len(tpls))
} }
} }
func TestRenderDependency(t *testing.T) { func TestRenderDependency(t *testing.T) {
e := New()
deptpl := `{{define "myblock"}}World{{end}}` deptpl := `{{define "myblock"}}World{{end}}`
toptpl := `Hello {{template "myblock"}}` toptpl := `Hello {{template "myblock"}}`
ch := &chart.Chart{ ch := &chart.Chart{
@ -245,7 +234,7 @@ func TestRenderDependency(t *testing.T) {
}, },
}) })
out, err := e.Render(ch, map[string]interface{}{}) out, err := Render(ch, map[string]interface{}{})
if err != nil { if err != nil {
t.Fatalf("failed to render chart: %s", err) t.Fatalf("failed to render chart: %s", err)
} }
@ -262,8 +251,6 @@ func TestRenderDependency(t *testing.T) {
} }
func TestRenderNestedValues(t *testing.T) { func TestRenderNestedValues(t *testing.T) {
e := New()
innerpath := "templates/inner.tpl" innerpath := "templates/inner.tpl"
outerpath := "templates/outer.tpl" outerpath := "templates/outer.tpl"
// Ensure namespacing rules are working. // Ensure namespacing rules are working.
@ -330,7 +317,7 @@ func TestRenderNestedValues(t *testing.T) {
t.Logf("Calculated values: %v", inject) t.Logf("Calculated values: %v", inject)
out, err := e.Render(outer, inject) out, err := Render(outer, inject)
if err != nil { if err != nil {
t.Fatalf("failed to render templates: %s", err) t.Fatalf("failed to render templates: %s", err)
} }
@ -387,7 +374,7 @@ func TestRenderBuiltinValues(t *testing.T) {
t.Logf("Calculated values: %v", outer) t.Logf("Calculated values: %v", outer)
out, err := New().Render(outer, inject) out, err := Render(outer, inject)
if err != nil { if err != nil {
t.Fatalf("failed to render templates: %s", err) t.Fatalf("failed to render templates: %s", err)
} }
@ -422,7 +409,7 @@ func TestAlterFuncMap_include(t *testing.T) {
}, },
} }
out, err := New().Render(c, v) out, err := Render(c, v)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -453,7 +440,7 @@ func TestAlterFuncMap_require(t *testing.T) {
}, },
} }
out, err := New().Render(c, v) out, err := Render(c, v)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -486,7 +473,7 @@ func TestAlterFuncMap_tpl(t *testing.T) {
}, },
} }
out, err := New().Render(c, v) out, err := Render(c, v)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -515,7 +502,7 @@ func TestAlterFuncMap_tplfunc(t *testing.T) {
}, },
} }
out, err := New().Render(c, v) out, err := Render(c, v)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -544,7 +531,7 @@ func TestAlterFuncMap_tplinclude(t *testing.T) {
}, },
} }
out, err := New().Render(c, v) out, err := Render(c, v)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -1,10 +1,11 @@
/* /*
Copyright The Helm Authors. Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
@ -13,28 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package chartutil package engine
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"encoding/json"
"path" "path"
"strings" "strings"
"github.com/BurntSushi/toml"
"github.com/ghodss/yaml"
"github.com/gobwas/glob" "github.com/gobwas/glob"
"k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/chart"
) )
// Files is a map of files in a chart that can be accessed from a template. // files is a map of files in a chart that can be accessed from a template.
type Files map[string][]byte type files map[string][]byte
// NewFiles creates a new Files from chart files. // NewFiles creates a new files from chart files.
// Given an []*any.Any (the format for files in a chart.Chart), extract a map of files. // Given an []*any.Any (the format for files in a chart.Chart), extract a map of files.
func NewFiles(from []*chart.File) Files { func newFiles(from []*chart.File) files {
files := make(map[string][]byte) files := make(map[string][]byte)
for _, f := range from { for _, f := range from {
files[f.Name] = f.Data files[f.Name] = f.Data
@ -49,7 +46,7 @@ func NewFiles(from []*chart.File) Files {
// //
// This is intended to be accessed from within a template, so a missed key returns // This is intended to be accessed from within a template, so a missed key returns
// an empty []byte. // an empty []byte.
func (f Files) GetBytes(name string) []byte { func (f files) GetBytes(name string) []byte {
if v, ok := f[name]; ok { if v, ok := f[name]; ok {
return v return v
} }
@ -62,7 +59,7 @@ func (f Files) GetBytes(name string) []byte {
// template. // template.
// //
// {{.Files.Get "foo"}} // {{.Files.Get "foo"}}
func (f Files) Get(name string) string { func (f files) Get(name string) string {
return string(f.GetBytes(name)) return string(f.GetBytes(name))
} }
@ -74,13 +71,13 @@ func (f Files) Get(name string) string {
// {{ range $name, $content := .Files.Glob("foo/**") }} // {{ range $name, $content := .Files.Glob("foo/**") }}
// {{ $name }}: | // {{ $name }}: |
// {{ .Files.Get($name) | indent 4 }}{{ end }} // {{ .Files.Get($name) | indent 4 }}{{ end }}
func (f Files) Glob(pattern string) Files { func (f files) Glob(pattern string) files {
g, err := glob.Compile(pattern, '/') g, err := glob.Compile(pattern, '/')
if err != nil { if err != nil {
g, _ = glob.Compile("**") g, _ = glob.Compile("**")
} }
nf := NewFiles(nil) nf := newFiles(nil)
for name, contents := range f { for name, contents := range f {
if g.Match(name) { if g.Match(name) {
nf[name] = contents nf[name] = contents
@ -96,7 +93,7 @@ func (f Files) Glob(pattern string) Files {
// (regardless of path) should be unique. // (regardless of path) should be unique.
// //
// This is designed to be called from a template, and will return empty string // This is designed to be called from a template, and will return empty string
// (via ToYAML function) if it cannot be serialized to YAML, or if the Files // (via toYAML function) if it cannot be serialized to YAML, or if the Files
// object is nil. // object is nil.
// //
// The output will not be indented, so you will want to pipe this to the // The output will not be indented, so you will want to pipe this to the
@ -104,7 +101,7 @@ func (f Files) Glob(pattern string) Files {
// //
// data: // data:
// {{ .Files.Glob("config/**").AsConfig() | indent 4 }} // {{ .Files.Glob("config/**").AsConfig() | indent 4 }}
func (f Files) AsConfig() string { func (f files) AsConfig() string {
if f == nil { if f == nil {
return "" return ""
} }
@ -116,7 +113,7 @@ func (f Files) AsConfig() string {
m[path.Base(k)] = string(v) m[path.Base(k)] = string(v)
} }
return ToYAML(m) return toYAML(m)
} }
// AsSecrets returns the base64-encoded value of a Files object suitable for // AsSecrets returns the base64-encoded value of a Files object suitable for
@ -125,7 +122,7 @@ func (f Files) AsConfig() string {
// (regardless of path) should be unique. // (regardless of path) should be unique.
// //
// This is designed to be called from a template, and will return empty string // This is designed to be called from a template, and will return empty string
// (via ToYAML function) if it cannot be serialized to YAML, or if the Files // (via toYAML function) if it cannot be serialized to YAML, or if the Files
// object is nil. // object is nil.
// //
// The output will not be indented, so you will want to pipe this to the // The output will not be indented, so you will want to pipe this to the
@ -133,7 +130,7 @@ func (f Files) AsConfig() string {
// //
// data: // data:
// {{ .Files.Glob("secrets/*").AsSecrets() }} // {{ .Files.Glob("secrets/*").AsSecrets() }}
func (f Files) AsSecrets() string { func (f files) AsSecrets() string {
if f == nil { if f == nil {
return "" return ""
} }
@ -144,7 +141,7 @@ func (f Files) AsSecrets() string {
m[path.Base(k)] = base64.StdEncoding.EncodeToString(v) m[path.Base(k)] = base64.StdEncoding.EncodeToString(v)
} }
return ToYAML(m) return toYAML(m)
} }
// Lines returns each line of a named file (split by "\n") as a slice, so it can // Lines returns each line of a named file (split by "\n") as a slice, so it can
@ -154,80 +151,10 @@ func (f Files) AsSecrets() string {
// //
// {{ range .Files.Lines "foo/bar.html" }} // {{ range .Files.Lines "foo/bar.html" }}
// {{ . }}{{ end }} // {{ . }}{{ end }}
func (f Files) Lines(path string) []string { func (f files) Lines(path string) []string {
if f == nil || f[path] == nil { if f == nil || f[path] == nil {
return []string{} return []string{}
} }
return strings.Split(string(f[path]), "\n") return strings.Split(string(f[path]), "\n")
} }
// ToYAML takes an interface, marshals it to yaml, and returns a string. It will
// always return a string, even on marshal error (empty string).
//
// This is designed to be called from a template.
func ToYAML(v interface{}) string {
data, err := yaml.Marshal(v)
if err != nil {
// Swallow errors inside of a template.
return ""
}
return strings.TrimSuffix(string(data), "\n")
}
// FromYAML converts a YAML document into a map[string]interface{}.
//
// This is not a general-purpose YAML parser, and will not parse all valid
// YAML documents. Additionally, because its intended use is within templates
// it tolerates errors. It will insert the returned error message string into
// m["Error"] in the returned map.
func FromYAML(str string) map[string]interface{} {
m := map[string]interface{}{}
if err := yaml.Unmarshal([]byte(str), &m); err != nil {
m["Error"] = err.Error()
}
return m
}
// ToTOML takes an interface, marshals it to toml, and returns a string. It will
// always return a string, even on marshal error (empty string).
//
// This is designed to be called from a template.
func ToTOML(v interface{}) string {
b := bytes.NewBuffer(nil)
e := toml.NewEncoder(b)
err := e.Encode(v)
if err != nil {
return err.Error()
}
return b.String()
}
// ToJSON takes an interface, marshals it to json, and returns a string. It will
// always return a string, even on marshal error (empty string).
//
// This is designed to be called from a template.
func ToJSON(v interface{}) string {
data, err := json.Marshal(v)
if err != nil {
// Swallow errors inside of a template.
return ""
}
return string(data)
}
// FromJSON converts a JSON document into a map[string]interface{}.
//
// This is not a general-purpose JSON parser, and will not parse all valid
// JSON documents. Additionally, because its intended use is within templates
// it tolerates errors. It will insert the returned error message string into
// m["Error"] in the returned map.
func FromJSON(str string) map[string]interface{} {
m := make(map[string]interface{})
if err := json.Unmarshal([]byte(str), &m); err != nil {
m["Error"] = err.Error()
}
return m
}

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package chartutil package engine
import ( import (
"testing" "testing"
@ -31,8 +31,8 @@ var cases = []struct {
{"multiline/test.txt", "bar\nfoo"}, {"multiline/test.txt", "bar\nfoo"},
} }
func getTestFiles() Files { func getTestFiles() files {
a := make(Files, len(cases)) a := make(files, len(cases))
for _, c := range cases { for _, c := range cases {
a[c.path] = []byte(c.data) a[c.path] = []byte(c.data)
} }
@ -96,121 +96,3 @@ func TestLines(t *testing.T) {
as.Equal("bar", out[0]) as.Equal("bar", out[0])
} }
func TestToYAML(t *testing.T) {
expect := "foo: bar"
v := struct {
Foo string `json:"foo"`
}{
Foo: "bar",
}
if got := ToYAML(v); got != expect {
t.Errorf("Expected %q, got %q", expect, got)
}
}
func TestToTOML(t *testing.T) {
expect := "foo = \"bar\"\n"
v := struct {
Foo string `toml:"foo"`
}{
Foo: "bar",
}
if got := ToTOML(v); got != expect {
t.Errorf("Expected %q, got %q", expect, got)
}
// Regression for https://github.com/helm/helm/issues/2271
dict := map[string]map[string]string{
"mast": {
"sail": "white",
},
}
got := ToTOML(dict)
expect = "[mast]\n sail = \"white\"\n"
if got != expect {
t.Errorf("Expected:\n%s\nGot\n%s\n", expect, got)
}
}
func TestFromYAML(t *testing.T) {
doc := `hello: world
one:
two: three
`
dict := FromYAML(doc)
if err, ok := dict["Error"]; ok {
t.Fatalf("Parse error: %s", err)
}
if len(dict) != 2 {
t.Fatal("expected two elements.")
}
world := dict["hello"]
if world.(string) != "world" {
t.Fatal("Expected the world. Is that too much to ask?")
}
// This should fail because we don't currently support lists:
doc2 := `
- one
- two
- three
`
dict = FromYAML(doc2)
if _, ok := dict["Error"]; !ok {
t.Fatal("Expected parser error")
}
}
func TestToJSON(t *testing.T) {
expect := `{"foo":"bar"}`
v := struct {
Foo string `json:"foo"`
}{
Foo: "bar",
}
if got := ToJSON(v); got != expect {
t.Errorf("Expected %q, got %q", expect, got)
}
}
func TestFromJSON(t *testing.T) {
doc := `{
"hello": "world",
"one": {
"two": "three"
}
}
`
dict := FromJSON(doc)
if err, ok := dict["Error"]; ok {
t.Fatalf("Parse error: %s", err)
}
if len(dict) != 2 {
t.Fatal("expected two elements.")
}
world := dict["hello"]
if world.(string) != "world" {
t.Fatal("Expected the world. Is that too much to ask?")
}
// This should fail because we don't currently support lists:
doc2 := `
[
"one",
"two",
"three"
]
`
dict = FromJSON(doc2)
if _, ok := dict["Error"]; !ok {
t.Fatal("Expected parser error")
}
}

@ -0,0 +1,152 @@
/*
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 (
"bytes"
"encoding/json"
"strings"
"text/template"
"github.com/BurntSushi/toml"
"github.com/Masterminds/sprig"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
// funcMap returns a mapping of all of the functions that Engine has.
//
// Because some functions are late-bound (e.g. contain context-sensitive
// data), the functions may not all perform identically outside of an Engine
// as they will inside of an Engine.
//
// Known late-bound functions:
//
// - "include"
// - "tpl"
//
// These are late-bound in Engine.Render(). The
// version included in the FuncMap is a placeholder.
//
func funcMap() template.FuncMap {
f := sprig.TxtFuncMap()
delete(f, "env")
delete(f, "expandenv")
// Add some extra functionality
extra := template.FuncMap{
"toToml": toTOML,
"toYaml": toYAML,
"fromYaml": fromYAML,
"toJson": toJSON,
"fromJson": fromJSON,
"required": required,
// This is a placeholder for the "include" function, which is
// late-bound to a template. By declaring it here, we preserve the
// integrity of the linter.
"include": func(string, interface{}) string { return "not implemented" },
"tpl": func(string, interface{}) interface{} { return "not implemented" },
}
for k, v := range extra {
f[k] = v
}
return f
}
func required(warn string, val interface{}) (interface{}, error) {
if val == nil {
return val, errors.Errorf(warn)
} else if _, ok := val.(string); ok {
if val == "" {
return val, errors.Errorf(warn)
}
}
return val, nil
}
// toYAML takes an interface, marshals it to yaml, and returns a string. It will
// always return a string, even on marshal error (empty string).
//
// This is designed to be called from a template.
func toYAML(v interface{}) string {
data, err := yaml.Marshal(v)
if err != nil {
// Swallow errors inside of a template.
return ""
}
return strings.TrimSuffix(string(data), "\n")
}
// fromYAML converts a YAML document into a map[string]interface{}.
//
// This is not a general-purpose YAML parser, and will not parse all valid
// YAML documents. Additionally, because its intended use is within templates
// it tolerates errors. It will insert the returned error message string into
// m["Error"] in the returned map.
func fromYAML(str string) map[string]interface{} {
m := map[string]interface{}{}
if err := yaml.Unmarshal([]byte(str), &m); err != nil {
m["Error"] = err.Error()
}
return m
}
// toTOML takes an interface, marshals it to toml, and returns a string. It will
// always return a string, even on marshal error (empty string).
//
// This is designed to be called from a template.
func toTOML(v interface{}) string {
b := bytes.NewBuffer(nil)
e := toml.NewEncoder(b)
err := e.Encode(v)
if err != nil {
return err.Error()
}
return b.String()
}
// toJSON takes an interface, marshals it to json, and returns a string. It will
// always return a string, even on marshal error (empty string).
//
// This is designed to be called from a template.
func toJSON(v interface{}) string {
data, err := json.Marshal(v)
if err != nil {
// Swallow errors inside of a template.
return ""
}
return string(data)
}
// fromJSON converts a JSON document into a map[string]interface{}.
//
// This is not a general-purpose JSON parser, and will not parse all valid
// JSON documents. Additionally, because its intended use is within templates
// it tolerates errors. It will insert the returned error message string into
// m["Error"] in the returned map.
func fromJSON(str string) map[string]interface{} {
m := make(map[string]interface{})
if err := json.Unmarshal([]byte(str), &m); err != nil {
m["Error"] = err.Error()
}
return m
}

@ -0,0 +1,77 @@
/*
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 (
"strings"
"testing"
"text/template"
"github.com/stretchr/testify/assert"
)
func TestFuncs(t *testing.T) {
//TODO write tests for failure cases
tests := []struct {
tpl, expect string
vars interface{}
}{{
tpl: `All {{ required "A valid 'bases' is required" .bases }} of them!`,
expect: `All 2 of them!`,
vars: map[string]interface{}{"bases": 2},
}, {
tpl: `{{ toYaml . }}`,
expect: `foo: bar`,
vars: map[string]interface{}{"foo": "bar"},
}, {
tpl: `{{ toToml . }}`,
expect: "foo = \"bar\"\n",
vars: map[string]interface{}{"foo": "bar"},
}, {
tpl: `{{ toJson . }}`,
expect: `{"foo":"bar"}`,
vars: map[string]interface{}{"foo": "bar"},
}, {
tpl: `{{ fromYaml . }}`,
expect: "map[hello:world]",
vars: `hello: world`,
}, {
// Regression for https://github.com/helm/helm/issues/2271
tpl: `{{ toToml . }}`,
expect: "[mast]\n sail = \"white\"\n",
vars: map[string]map[string]string{"mast": {"sail": "white"}},
}, {
tpl: `{{ fromYaml . }}`,
expect: "map[Error:yaml: unmarshal errors:\n line 1: cannot unmarshal !!seq into map[string]interface {}]",
vars: "- one\n- two\n",
}, {
tpl: `{{ fromJson .}}`,
expect: `map[hello:world]`,
vars: `{"hello":"world"}`,
}, {
tpl: `{{ fromJson . }}`,
expect: `map[Error:json: cannot unmarshal array into Go value of type map[string]interface {}]`,
vars: `["one", "two"]`,
}}
for _, tt := range tests {
var b strings.Builder
err := template.Must(template.New("test").Funcs(funcMap()).Parse(tt.tpl)).Execute(&b, tt.vars)
assert.NoError(t, err)
assert.Equal(t, tt.expect, b.String(), tt.tpl)
}
}

@ -64,10 +64,8 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
//linter.RunLinterRule(support.ErrorSev, err) //linter.RunLinterRule(support.ErrorSev, err)
return return
} }
e := engine.New() var e engine.Engine
if strict { e.Strict = strict
e.Strict = true
}
renderedContentMap, err := e.Render(chart, valuesToRender) renderedContentMap, err := e.Render(chart, valuesToRender)
renderOk := linter.RunLinterRule(support.ErrorSev, path, err) renderOk := linter.RunLinterRule(support.ErrorSev, path, err)

@ -92,7 +92,7 @@ type ReleaseServer struct {
// NewReleaseServer creates a new release server. // NewReleaseServer creates a new release server.
func NewReleaseServer(discovery discovery.DiscoveryInterface, kubeClient environment.KubeClient) *ReleaseServer { func NewReleaseServer(discovery discovery.DiscoveryInterface, kubeClient environment.KubeClient) *ReleaseServer {
return &ReleaseServer{ return &ReleaseServer{
engine: engine.New(), engine: new(engine.Engine),
discovery: discovery, discovery: discovery,
Releases: storage.Init(driver.NewMemory()), Releases: storage.Init(driver.NewMemory()),
KubeClient: kubeClient, KubeClient: kubeClient,

@ -484,7 +484,7 @@ func (kc *mockHooksKubeClient) WaitAndGetCompletedPodPhase(_ string, _ io.Reader
func deletePolicyStub(kubeClient *mockHooksKubeClient) *ReleaseServer { func deletePolicyStub(kubeClient *mockHooksKubeClient) *ReleaseServer {
return &ReleaseServer{ return &ReleaseServer{
engine: engine.New(), engine: new(engine.Engine),
discovery: fake.NewSimpleClientset().Discovery(), discovery: fake.NewSimpleClientset().Discovery(),
KubeClient: kubeClient, KubeClient: kubeClient,
Log: func(_ string, _ ...interface{}) {}, Log: func(_ string, _ ...interface{}) {},

Loading…
Cancel
Save