From fa387494fb5d454501944c5b7f563cc8ba91b291 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 12 Apr 2016 16:18:42 -0600 Subject: [PATCH 1/2] 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() +} From 913905a54fd27e9bc388f50b4fd2e5951ea27aea Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 12 Apr 2016 16:58:53 -0600 Subject: [PATCH 2/2] fix(tiller): change environment.Engine signature --- cmd/tiller/environment/environment.go | 5 ++++- cmd/tiller/environment/environment_test.go | 10 +++++----- pkg/engine/engine_test.go | 4 ++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/cmd/tiller/environment/environment.go b/cmd/tiller/environment/environment.go index e92eb77cc..f7d863b6d 100644 --- a/cmd/tiller/environment/environment.go +++ b/cmd/tiller/environment/environment.go @@ -17,10 +17,13 @@ func (y EngineYard) Get(k string) (Engine, bool) { // For some engines, "rendering" includes both compiling and executing. (Other // engines do not distinguish between phases.) // +// The engine returns a map where the key is the named output entity (usually +// a file name) and the value is the rendered content of the template. +// // An Engine must be capable of executing multiple concurrent requests, but // without tainting one request's environment with data from another request. type Engine interface { - Render(*hapi.Chart, *hapi.Values) ([]byte, error) + Render(*hapi.Chart, *hapi.Values) (map[string]string, error) } // ReleaseStorage represents a storage engine for a Release. diff --git a/cmd/tiller/environment/environment_test.go b/cmd/tiller/environment/environment_test.go index df240f209..e42376f6e 100644 --- a/cmd/tiller/environment/environment_test.go +++ b/cmd/tiller/environment/environment_test.go @@ -7,10 +7,10 @@ import ( ) type mockEngine struct { - out []byte + out map[string]string } -func (e *mockEngine) Render(chrt *hapi.Chart, v *hapi.Values) ([]byte, error) { +func (e *mockEngine) Render(chrt *hapi.Chart, v *hapi.Values) (map[string]string, error) { return e.out, nil } @@ -39,7 +39,7 @@ var _ ReleaseStorage = &mockReleaseStorage{} var _ KubeClient = &mockKubeClient{} func TestEngine(t *testing.T) { - eng := &mockEngine{out: []byte("test")} + eng := &mockEngine{out: map[string]string{"albatross": "test"}} env := New() env.EngineYard = EngineYard(map[string]Engine{"test": eng}) @@ -48,8 +48,8 @@ func TestEngine(t *testing.T) { t.Errorf("failed to get engine from EngineYard") } else if out, err := engine.Render(&hapi.Chart{}, &hapi.Values{}); err != nil { t.Errorf("unexpected template error: %s", err) - } else if string(out) != "test" { - t.Errorf("expected 'test', got %q", string(out)) + } else if out["albatross"] != "test" { + t.Errorf("expected 'test', got %q", out["albatross"]) } } diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index b0a00794b..613804ef3 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -4,8 +4,12 @@ import ( "fmt" "sync" "testing" + + "github.com/deis/tiller/cmd/tiller/environment" ) +var _ environment.Engine = &Engine{} + func TestEngine(t *testing.T) { e := New()