diff --git a/cmd/helm/template.go b/cmd/helm/template.go index f89aaed2b..06a7f355c 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -175,9 +175,6 @@ func (o *templateOptions) run(out io.Writer) error { return err } - // Set up engine. - renderer := engine.New() - // kubernetes version kv, err := semver.NewVersion(o.kubeVersion) if err != nil { @@ -194,7 +191,7 @@ func (o *templateOptions) run(out io.Writer) error { return err } - rendered, err := renderer.Render(c, vals) + rendered, err := engine.Render(c, vals) if err != nil { return err } diff --git a/pkg/action/install.go b/pkg/action/install.go index 4fac26ae5..dd0acdf31 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -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 { return hooks, buf, "", err } diff --git a/pkg/chartutil/dependencies_test.go b/pkg/chartutil/dependencies_test.go index 55a3efc78..ea6ce066d 100644 --- a/pkg/chartutil/dependencies_test.go +++ b/pkg/chartutil/dependencies_test.go @@ -218,17 +218,17 @@ func TestProcessDependencyImportValues(t *testing.T) { t.Fatalf("retrieving import values table %v %v", kk, err) } - switch pv.(type) { + switch pv := pv.(type) { 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) } 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) } default: - if pv.(string) != vv { + if pv != vv { t.Errorf("failed to match imported string value %q with expected %q", pv, vv) } } diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index da5395f9e..8c7328701 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -286,11 +286,12 @@ func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} { // values. for key, val := range src { if istable(val) { - if innerdst, ok := dst[key]; !ok { + switch innerdst, ok := dst[key]; { + case !ok: dst[key] = val - } else if istable(innerdst) { + case istable(innerdst): 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) } } 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) { top := map[string]interface{}{ + "Chart": chrt.Metadata, + "Capabilities": caps, "Release": map[string]interface{}{ "Name": options.Name, "IsUpgrade": options.IsUpgrade, "IsInstall": options.IsInstall, "Service": "Helm", }, - "Chart": chrt.Metadata, - "Files": NewFiles(chrt.Files), - "Capabilities": caps, } vals, err := CoalesceValues(chrt, chrtVals) diff --git a/pkg/chartutil/values_test.go b/pkg/chartutil/values_test.go index f397009fc..2d56f771a 100644 --- a/pkg/chartutil/values_test.go +++ b/pkg/chartutil/values_test.go @@ -128,9 +128,6 @@ func TestToRenderValues(t *testing.T) { if !relmap["IsInstall"].(bool) { 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") { t.Error("Expected Capabilities to have v1 as an API") } diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 2d533fdff..a34e49888 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -22,7 +22,6 @@ import ( "strings" "text/template" - "github.com/Masterminds/sprig" "github.com/pkg/errors" "k8s.io/helm/pkg/chart" @@ -31,66 +30,9 @@ import ( // Engine is an implementation of 'cmd/tiller/environment'.Engine that uses Go templates. 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 // a value that was not passed in. - Strict bool - currentTemplates map[string]renderable -} - -// 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 + Strict bool } // Render takes a chart, optional values, and value overrides, and attempts to render the Go templates. @@ -112,11 +54,15 @@ func FuncMap() template.FuncMap { // 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) { - // Render the charts +func (e Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) { tmap := allTemplates(chrt, values) - e.currentTemplates = tmap - return e.render(chrt, 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. @@ -129,15 +75,9 @@ type renderable struct { basePath string } -// alterFuncMap takes the Engine's FuncMap and adds context-specific functions. -// -// The resulting FuncMap is only valid for the passed-in template. -func (e *Engine) alterFuncMap(t *template.Template) 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 - } +// initFunMap creates the Engine's FuncMap and adds context-specific functions. +func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]renderable) { + funcMap := funcMap() // Add the 'include' function here so we can close over t. funcMap["include"] = func(name string, data interface{}) (string, error) { @@ -146,18 +86,6 @@ func (e *Engine) alterFuncMap(t *template.Template) template.FuncMap { 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 funcMap["tpl"] = func(tpl string, vals chartutil.Values) (string, error) { basePath, err := vals.PathValue("Template.BasePath") @@ -165,32 +93,36 @@ func (e *Engine) alterFuncMap(t *template.Template) template.FuncMap { 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") if err != nil { return "", errors.Wrapf(err, "cannot retrieve Template.Name from values inside tpl function: %s", tpl) } - templates := make(map[string]renderable) - templates[templateName.(string)] = r + templates := map[string]renderable{ + templateName.(string): { + tpl: tpl, + vals: vals, + basePath: basePath.(string), + }, + } - result, err := e.render(nil, templates) + 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 } - - return funcMap + t.Funcs(funcMap) } // render takes a map of templates/values and renders them. -func (e *Engine) render(ch *chart.Chart, 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) +} + +// 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. @@ -212,34 +144,31 @@ func (e *Engine) render(ch *chart.Chart, tpls map[string]renderable) (rendered m t.Option("missingkey=zero") } - funcMap := e.alterFuncMap(t) + 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) - files := []string{} - for _, fname := range keys { 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) } - files = append(files, fname) } - // Adding the engine's currentTemplates to the template context + // Adding the reference templates to the template context // so they can be referenced in the tpl function - for fname, r := range e.currentTemplates { + for fname, r := range referenceTpls { 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) } } } - rendered = make(map[string]string, len(files)) - for _, file := range files { + rendered = make(map[string]string, len(keys)) + for _, file := 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(file), "_") { @@ -261,9 +190,6 @@ func (e *Engine) render(ch *chart.Chart, tpls map[string]renderable) (rendered m Data: []byte(strings.Replace(buf.String(), "", "", -1)), } rendered[file] = string(f.Data) - if ch != nil { - ch.Files = append(ch.Files, f) - } } return rendered, nil @@ -306,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 // scope. -func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals chartutil.Values) { - // This should never evaluate to a nil map. That will cause problems when - // values are appended later. - cvals := make(chartutil.Values) +func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) { + next := map[string]interface{}{ + "Chart": c.Metadata, + "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() { - cvals = parentVals - } else if c.Name() != "" { - cvals = map[string]interface{}{ - "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 - } + next["Values"] = vals["Values"] + } else if vs, err := vals.Table("Values." + c.Name()); err == nil { + next["Values"] = vs } for _, child := range c.Dependencies() { - recAllTpls(child, templates, cvals) + recAllTpls(child, templates, next) } newParentID := c.ChartFullPath() for _, t := range c.Templates { templates[path.Join(newParentID, t.Name)] = renderable{ tpl: string(t.Data), - vals: cvals, + vals: next, basePath: path.Join(newParentID, "templates"), } } diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 0c02ef24f..90f10d368 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -18,6 +18,7 @@ package engine import ( "fmt" + "strings" "sync" "testing" @@ -51,25 +52,16 @@ func TestSortTemplates(t *testing.T) { } for i, e := range expect { 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) - } - } -} - -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) + t.Fatalf("\n\tExp:\n%s\n\tGot:\n%s", + strings.Join(expect, "\n"), + strings.Join(got, "\n"), + ) } } } func TestFuncMap(t *testing.T) { - fns := FuncMap() + fns := funcMap() forbidden := []string{"env", "expandenv"} for _, f := range forbidden { if _, ok := fns[f]; ok { @@ -93,53 +85,49 @@ func TestRender(t *testing.T) { Version: "1.2.3", }, Templates: []*chart.File{ - {Name: "templates/test1", Data: []byte("{{.outer | title }} {{.inner | title}}")}, - {Name: "templates/test2", Data: []byte("{{.global.callme | lower }}")}, + {Name: "templates/test1", Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")}, + {Name: "templates/test2", Data: []byte("{{.Values.global.callme | lower }}")}, {Name: "templates/test3", Data: []byte("{{.noValue}}")}, + {Name: "templates/test4", Data: []byte("{{toJson .Values}}")}, }, Values: map[string]interface{}{"outer": "DEFAULT", "inner": "DEFAULT"}, } vals := map[string]interface{}{ - "outer": "spouter", - "inner": "inn", - "global": map[string]interface{}{ - "callme": "Ishmael", + "Values": map[string]interface{}{ + "outer": "spouter", + "inner": "inn", + "global": map[string]interface{}{ + "callme": "Ishmael", + }, }, } - e := New() v, err := chartutil.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } - out, err := e.Render(c, v) + out, err := Render(c, v) if err != nil { t.Errorf("Failed to render templates: %s", err) } - expect := "Spouter Inn" - if out["moby/templates/test1"] != expect { - t.Errorf("Expected %q, got %q", expect, out["test1"]) - } - - 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"]) + expect := map[string]string{ + "moby/templates/test1": "Spouter Inn", + "moby/templates/test2": "ishmael", + "moby/templates/test3": "", + "moby/templates/test4": `{"global":{"callme":"Ishmael"},"inner":"inn","outer":"spouter"}`, } - if _, err := e.Render(c, v); err != nil { - t.Errorf("Unexpected error: %s", err) + for name, data := range expect { + if out[name] != data { + t.Errorf("Expected %q, got %q", data, out[name]) + } } } func TestRenderInternals(t *testing.T) { // Test the internals of the rendering tool. - e := New() vals := chartutil.Values{"Name": "one", "Value": "two"} tpls := map[string]renderable{ @@ -150,7 +138,7 @@ func TestRenderInternals(t *testing.T) { "three": {tpl: `{{template "two" dict "Value" "three"}}`, vals: vals}, } - out, err := e.render(nil, tpls) + out, err := new(Engine).render(tpls) if err != nil { t.Fatalf("Failed template rendering: %s", err) } @@ -174,21 +162,24 @@ func TestRenderInternals(t *testing.T) { func TestParallelRenderInternals(t *testing.T) { // Make sure that we can use one Engine to run parallel template renders. - e := New() + e := new(Engine) var wg sync.WaitGroup for i := 0; i < 20; i++ { wg.Add(1) go func(i int) { - fname := "my/file/name" tt := fmt.Sprintf("expect-%d", i) - v := chartutil.Values{"val": tt} - tpls := map[string]renderable{fname: {tpl: `{{.val}}`, vals: v}} - out, err := e.render(nil, tpls) + tpls := map[string]renderable{ + "t": { + tpl: `{{.val}}`, + vals: map[string]interface{}{"val": tt}, + }, + } + out, err := e.render(tpls) if err != nil { t.Errorf("Failed to render %s: %s", tt, err) } - if out[fname] != tt { - t.Errorf("Expected %q, got %q", tt, out[fname]) + if out["t"] != tt { + t.Errorf("Expected %q, got %q", tt, out["t"]) } wg.Done() }(i) @@ -221,15 +212,13 @@ func TestAllTemplates(t *testing.T) { } dep1.AddDependency(dep2) - var v chartutil.Values - tpls := allTemplates(ch1, v) + tpls := allTemplates(ch1, chartutil.Values{}) if len(tpls) != 5 { t.Errorf("Expected 5 charts, got %d", len(tpls)) } } func TestRenderDependency(t *testing.T) { - e := New() deptpl := `{{define "myblock"}}World{{end}}` toptpl := `Hello {{template "myblock"}}` 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 { t.Fatalf("failed to render chart: %s", err) } @@ -262,8 +251,6 @@ func TestRenderDependency(t *testing.T) { } func TestRenderNestedValues(t *testing.T) { - e := New() - innerpath := "templates/inner.tpl" outerpath := "templates/outer.tpl" // Ensure namespacing rules are working. @@ -330,7 +317,7 @@ func TestRenderNestedValues(t *testing.T) { t.Logf("Calculated values: %v", inject) - out, err := e.Render(outer, inject) + out, err := Render(outer, inject) if err != nil { t.Fatalf("failed to render templates: %s", err) } @@ -387,7 +374,7 @@ func TestRenderBuiltinValues(t *testing.T) { t.Logf("Calculated values: %v", outer) - out, err := New().Render(outer, inject) + out, err := Render(outer, inject) if err != nil { 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 { 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 { 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 { 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 { 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 { t.Fatal(err) } diff --git a/pkg/chartutil/files.go b/pkg/engine/files.go similarity index 54% rename from pkg/chartutil/files.go rename to pkg/engine/files.go index e04e4f612..654ec6ada 100644 --- a/pkg/chartutil/files.go +++ b/pkg/engine/files.go @@ -1,10 +1,11 @@ /* 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 + 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, @@ -13,28 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -package chartutil +package engine import ( - "bytes" "encoding/base64" - "encoding/json" "path" "strings" - "github.com/BurntSushi/toml" - "github.com/ghodss/yaml" "github.com/gobwas/glob" "k8s.io/helm/pkg/chart" ) -// Files is a map of files in a chart that can be accessed from a template. -type Files map[string][]byte +// files is a map of files in a chart that can be accessed from a template. +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. -func NewFiles(from []*chart.File) Files { +func newFiles(from []*chart.File) files { files := make(map[string][]byte) for _, f := range from { 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 // an empty []byte. -func (f Files) GetBytes(name string) []byte { +func (f files) GetBytes(name string) []byte { if v, ok := f[name]; ok { return v } @@ -62,7 +59,7 @@ func (f Files) GetBytes(name string) []byte { // template. // // {{.Files.Get "foo"}} -func (f Files) Get(name string) string { +func (f files) Get(name string) string { return string(f.GetBytes(name)) } @@ -74,13 +71,13 @@ func (f Files) Get(name string) string { // {{ range $name, $content := .Files.Glob("foo/**") }} // {{ $name }}: | // {{ .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, '/') if err != nil { g, _ = glob.Compile("**") } - nf := NewFiles(nil) + nf := newFiles(nil) for name, contents := range f { if g.Match(name) { nf[name] = contents @@ -96,7 +93,7 @@ func (f Files) Glob(pattern string) Files { // (regardless of path) should be unique. // // 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. // // 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: // {{ .Files.Glob("config/**").AsConfig() | indent 4 }} -func (f Files) AsConfig() string { +func (f files) AsConfig() string { if f == nil { return "" } @@ -116,7 +113,7 @@ func (f Files) AsConfig() string { m[path.Base(k)] = string(v) } - return ToYAML(m) + return toYAML(m) } // 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. // // 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. // // 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: // {{ .Files.Glob("secrets/*").AsSecrets() }} -func (f Files) AsSecrets() string { +func (f files) AsSecrets() string { if f == nil { return "" } @@ -144,7 +141,7 @@ func (f Files) AsSecrets() string { 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 @@ -154,80 +151,10 @@ func (f Files) AsSecrets() string { // // {{ range .Files.Lines "foo/bar.html" }} // {{ . }}{{ end }} -func (f Files) Lines(path string) []string { +func (f files) Lines(path string) []string { if f == nil || f[path] == nil { return []string{} } 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 -} diff --git a/pkg/chartutil/files_test.go b/pkg/engine/files_test.go similarity index 50% rename from pkg/chartutil/files_test.go rename to pkg/engine/files_test.go index 9d1c4f7bb..4b37724f9 100644 --- a/pkg/chartutil/files_test.go +++ b/pkg/engine/files_test.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package chartutil +package engine import ( "testing" @@ -31,8 +31,8 @@ var cases = []struct { {"multiline/test.txt", "bar\nfoo"}, } -func getTestFiles() Files { - a := make(Files, len(cases)) +func getTestFiles() files { + a := make(files, len(cases)) for _, c := range cases { a[c.path] = []byte(c.data) } @@ -96,121 +96,3 @@ func TestLines(t *testing.T) { 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") - } -} diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go new file mode 100644 index 000000000..2b927872f --- /dev/null +++ b/pkg/engine/funcs.go @@ -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 +} diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go new file mode 100644 index 000000000..bbdd6ebe3 --- /dev/null +++ b/pkg/engine/funcs_test.go @@ -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) + } +} diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index 2acf63168..6b7f53578 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -64,10 +64,8 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace //linter.RunLinterRule(support.ErrorSev, err) return } - e := engine.New() - if strict { - e.Strict = true - } + var e engine.Engine + e.Strict = strict renderedContentMap, err := e.Render(chart, valuesToRender) renderOk := linter.RunLinterRule(support.ErrorSev, path, err) diff --git a/pkg/tiller/release_server.go b/pkg/tiller/release_server.go index 4134443fc..17051e342 100644 --- a/pkg/tiller/release_server.go +++ b/pkg/tiller/release_server.go @@ -92,7 +92,7 @@ type ReleaseServer struct { // NewReleaseServer creates a new release server. func NewReleaseServer(discovery discovery.DiscoveryInterface, kubeClient environment.KubeClient) *ReleaseServer { return &ReleaseServer{ - engine: engine.New(), + engine: new(engine.Engine), discovery: discovery, Releases: storage.Init(driver.NewMemory()), KubeClient: kubeClient, diff --git a/pkg/tiller/release_server_test.go b/pkg/tiller/release_server_test.go index 6e4652258..082c3a2db 100644 --- a/pkg/tiller/release_server_test.go +++ b/pkg/tiller/release_server_test.go @@ -484,7 +484,7 @@ func (kc *mockHooksKubeClient) WaitAndGetCompletedPodPhase(_ string, _ io.Reader func deletePolicyStub(kubeClient *mockHooksKubeClient) *ReleaseServer { return &ReleaseServer{ - engine: engine.New(), + engine: new(engine.Engine), discovery: fake.NewSimpleClientset().Discovery(), KubeClient: kubeClient, Log: func(_ string, _ ...interface{}) {},