From fa387494fb5d454501944c5b7f563cc8ba91b291 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 12 Apr 2016 16:18:42 -0600 Subject: [PATCH] feat(engine): add template engine --- glide.lock | 8 +++- glide.yaml | 1 + pkg/engine/doc.go | 7 ++++ pkg/engine/engine.go | 73 ++++++++++++++++++++++++++++++++++ pkg/engine/engine_test.go | 82 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 pkg/engine/doc.go create mode 100644 pkg/engine/engine.go create mode 100644 pkg/engine/engine_test.go diff --git a/glide.lock b/glide.lock index 2a820a40d..0aaa8649f 100644 --- a/glide.lock +++ b/glide.lock @@ -1,12 +1,16 @@ -hash: 692bf17990973cf424c2dbfda6afee8b7002bbc07aaaa78372eac447fed4c8b0 -updated: 2016-04-11T16:13:14.192550144-06:00 +hash: 1dcbbc192182125021b40497fcf9d52bc643455a3bb03d6cd3458819fcb03dbb +updated: 2016-04-12T13:27:50.987288211-06:00 imports: +- name: github.com/aokoli/goutils + version: 9c37978a95bd5c709a15883b6242714ea6709e64 - name: github.com/codegangsta/cli version: 71f57d300dd6a780ac1856c005c4b518cfd498ec - name: github.com/golang/protobuf version: dda510ac0fd43b39770f22ac6260eb91d377bce3 subpackages: - proto +- name: github.com/Masterminds/sprig + version: 679bb747f11c6ffc3373965988fea8877c40b47b - name: github.com/spf13/cobra version: 4c05eb1145f16d0e6bb4a3e1b6d769f4713cb41f subpackages: diff --git a/glide.yaml b/glide.yaml index bb9ca005c..dab2d29c8 100644 --- a/glide.yaml +++ b/glide.yaml @@ -7,3 +7,4 @@ import: - package: github.com/spf13/cobra subpackages: - cobra +- package: github.com/Masterminds/sprig diff --git a/pkg/engine/doc.go b/pkg/engine/doc.go new file mode 100644 index 000000000..e657b1734 --- /dev/null +++ b/pkg/engine/doc.go @@ -0,0 +1,7 @@ +/*Package engine implements the Go template engine as a Tiller Engine. + +Tiller provides a simple interface for taking a Chart and rendering its templates. +The 'engine' package implements this interface using Go's built-in 'text/template' +package. +*/ +package engine diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go new file mode 100644 index 000000000..067d6b98a --- /dev/null +++ b/pkg/engine/engine.go @@ -0,0 +1,73 @@ +package engine + +import ( + "bytes" + "fmt" + "text/template" + + "github.com/Masterminds/sprig" + "github.com/deis/tiller/pkg/hapi" +) + +type Engine struct { + FuncMap template.FuncMap +} + +// 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 { + f := sprig.TxtFuncMap() + delete(f, "env") + delete(f, "expandenv") + return &Engine{ + FuncMap: f, + } +} + +// Render takes a chart, optional values, 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. +func (e *Engine) Render(chart *hapi.Chart, vals *hapi.Values) (map[string]string, error) { + // Uncomment this once the proto files compile. + //return render(chart.Chartfile.Name, chart.Templates, vals) + return map[string]string{}, nil +} + +func (e *Engine) render(name string, tpls map[string]string, v interface{}) (map[string]string, 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. + t := template.New(name) + files := []string{} + for fname, tpl := range tpls { + t = t.New(fname).Funcs(e.FuncMap) + if _, err := t.Parse(tpl); err != nil { + return map[string]string{}, fmt.Errorf("parse error in %q: %s", fname, err) + } + files = append(files, fname) + } + + rendered := make(map[string]string, len(files)) + var buf bytes.Buffer + for _, file := range files { + if err := t.ExecuteTemplate(&buf, file, v); err != nil { + return map[string]string{}, fmt.Errorf("render error in %q: %s", file, err) + } + rendered[file] = buf.String() + buf.Reset() + } + + return rendered, nil +} diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go new file mode 100644 index 000000000..b0a00794b --- /dev/null +++ b/pkg/engine/engine_test.go @@ -0,0 +1,82 @@ +package engine + +import ( + "fmt" + "sync" + "testing" +) + +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 TestRender(t *testing.T) { + t.Skip() +} + +func TestRenderInternals(t *testing.T) { + // Test the internals of the rendering tool. + e := New() + + tpls := map[string]string{ + "one": `Hello {{title .Name}}`, + "two": `Goodbye {{upper .Value}}`, + // Test whether a template can reliably reference another template + // without regard for ordering. + "three": `{{template "two" dict "Value" "three"}}`, + } + vals := map[string]string{"Name": "one", "Value": "two"} + + out, err := e.render("irrelevant", tpls, vals) + if err != nil { + t.Fatalf("Failed template rendering: %s", err) + } + + if len(out) != 3 { + t.Fatalf("Expected 3 templates, got %d", len(out)) + } + + if out["one"] != "Hello One" { + t.Errorf("Expected 'Hello One', got %q", out["one"]) + } + + if out["two"] != "Goodbye TWO" { + t.Errorf("Expected 'Goodbye TWO'. got %q", out["two"]) + } + + if out["three"] != "Goodbye THREE" { + t.Errorf("Expected 'Goodbye THREE'. got %q", out["two"]) + } +} + +func TestParallelRenderInternals(t *testing.T) { + // Make sure that we can use one Engine to run parallel template renders. + e := New() + 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) + tpls := map[string]string{fname: `{{.val}}`} + v := map[string]string{"val": tt} + out, err := e.render("intentionally_duplicated", tpls, v) + 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]) + } + wg.Done() + }(i) + } + wg.Wait() +}