Skip to content
This repository has been archived by the owner on Jan 21, 2020. It is now read-only.

Commit

Permalink
Template improvements (#390)
Browse files Browse the repository at this point in the history
Signed-off-by: David Chung <[email protected]>
  • Loading branch information
David Chung authored Feb 8, 2017
1 parent 908e9fb commit b919e4f
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 44 deletions.
4 changes: 2 additions & 2 deletions examples/flavor/swarm/flavor.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (s *baseFlavor) prepare(role string, flavorProperties *types.Any, instanceS
log.Warningln("Worker prepare:", err)
}

swarmID := "?"
swarmID = "?"
if swarmStatus != nil {
swarmID = swarmStatus.ID
}
Expand Down Expand Up @@ -342,7 +342,7 @@ func (c *templateContext) Funcs() []template.Function {
},
},
{
Name: "SWARM_MANAGER_IP",
Name: "SWARM_MANAGER_ADDR",
Description: []string{"IP of the Swarm manager / leader"},
Func: func() (string, error) {
if c.nodeInfo == nil {
Expand Down
6 changes: 3 additions & 3 deletions examples/flavor/swarm/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ EOF
kill -s HUP $(cat /var/run/docker.pid)
sleep 5
{{ if eq INSTANCE_LOGICAL_ID SPEC.SwarmJoinIP }}
{{ if and ( eq INSTANCE_LOGICAL_ID SPEC.SwarmJoinIP ) (not SWARM_INITIALIZED) }}
{{/* The first node of the special allocations will initialize the swarm. */}}
docker swarm init --advertise-addr {{ INSTANCE_LOGICAL_ID }}
Expand All @@ -34,7 +34,7 @@ sleep 5
{{ else }}
{{/* The rest of the nodes will join as followers in the manager group. */}}
docker swarm join --token {{ SWARM_JOIN_TOKENS.Manager }} {{ SPEC.SwarmJoinIP }}:2377
docker swarm join --token {{ SWARM_JOIN_TOKENS.Manager }} {{ SWARM_MANAGER_ADDR }}
{{ end }}
`
Expand All @@ -59,7 +59,7 @@ kill -s HUP $(cat /var/run/docker.pid)
sleep 5
docker swarm join --token {{ SWARM_JOIN_TOKENS.Worker }} {{ SPEC.SwarmJoinIP }}:2377
docker swarm join --token {{ SWARM_JOIN_TOKENS.Worker }} {{ SWARM_MANAGER_ADDR }}
`
)
92 changes: 57 additions & 35 deletions pkg/template/funcs.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package template

import (
"bytes"
"encoding/json"
"fmt"
"reflect"
Expand All @@ -10,6 +11,24 @@ import (
"github.com/jmespath/go-jmespath"
)

// DeepCopyObject makes a deep copy of the argument, using encoding/gob encode/decode.
func DeepCopyObject(from interface{}) (interface{}, error) {
var mod bytes.Buffer
enc := json.NewEncoder(&mod)
dec := json.NewDecoder(&mod)
err := enc.Encode(from)
if err != nil {
return nil, err
}

copy := reflect.New(reflect.TypeOf(from))
err = dec.Decode(copy.Interface())
if err != nil {
return nil, err
}
return reflect.Indirect(copy).Interface(), nil
}

// QueryObject applies a JMESPath query specified by the expression, against the target object.
func QueryObject(exp string, target interface{}) (interface{}, error) {
query, err := jmespath.Compile(exp)
Expand Down Expand Up @@ -119,8 +138,14 @@ func (t *Template) DefaultFuncs() []Function {
Description: []string{
"Source / evaluate the template at the input location (as URL).",
"This will make all of the global variables declared there visible in this template's context.",
"Similar to 'source' in bash, sourcing another template means applying it in the same context ",
"as the calling template. The context (e.g. variables) of the calling template as a result can be mutated.",
},
Func: func(p string) (string, error) {
Func: func(p string, opt ...interface{}) (string, error) {
var o interface{}
if len(opt) > 0 {
o = opt[0]
}
loc := p
if strings.Index(loc, "str://") == -1 {
buff, err := getURL(t.url, p)
Expand All @@ -133,48 +158,54 @@ func (t *Template) DefaultFuncs() []Function {
if err != nil {
return "", err
}
// copy the binds in the parent scope into the child
for k, v := range t.binds {
sourced.binds[k] = v
}
// inherit the functions defined for this template
for k, v := range t.funcs {
sourced.AddFunc(k, v)
}
// set this as the parent of the sourced template so its global can mutate the globals in this
sourced.parent = t
sourced.forkFrom(t)
sourced.context = t.context

if o == nil {
o = sourced.context
}
// TODO(chungers) -- let the sourced template define new functions that can be called in the parent.
return sourced.Render(nil)
return sourced.Render(o)
},
},
{
Name: "include",
Description: []string{
"Render content found at URL as template and include here.",
"The optional second parameter is the context to use when rendering the template.",
"Conceptually similar to exec in bash, where the template included is applied using a fork ",
"of current context in the calling template. Any mutations to the context via 'global' will not ",
"be visible in the calling template's context.",
},
Func: func(p string, opt ...interface{}) (string, error) {
var o interface{}
if len(opt) > 0 {
o = opt[0]
}
loc, err := getURL(t.url, p)
if err != nil {
return "", err
loc := p
if strings.Index(loc, "str://") == -1 {
buff, err := getURL(t.url, p)
if err != nil {
return "", err
}
loc = buff
}
included, err := NewTemplate(loc, t.options)
if err != nil {
return "", err
}
// copy the binds in the parent scope into the child
for k, v := range t.binds {
included.binds[k] = v
dotCopy, err := included.forkFrom(t)
if err != nil {
return "", err
}
// inherit the functions defined for this template
for k, v := range t.funcs {
included.AddFunc(k, v)
included.context = dotCopy

if o == nil {
o = included.context
}

return included.Render(o)
},
},
Expand All @@ -193,10 +224,10 @@ func (t *Template) DefaultFuncs() []Function {
"Defines a variable with the first argument as name and last argument value as the default.",
"It's also ok to pass a third optional parameter, in the middle, as the documentation string.",
},
Func: func(name string, args ...interface{}) (string, error) {
Func: func(name string, args ...interface{}) (Void, error) {
if _, has := t.defaults[name]; has {
// not sure if this is good, but should complain loudly
return "", fmt.Errorf("already defined: %v", name)
return voidValue, fmt.Errorf("already defined: %v", name)
}
var doc string
var value interface{}
Expand All @@ -210,7 +241,7 @@ func (t *Template) DefaultFuncs() []Function {
value = args[1]
}
t.AddDef(name, value, doc)
return "", nil
return voidValue, nil
},
},
{
Expand All @@ -220,11 +251,9 @@ func (t *Template) DefaultFuncs() []Function {
"This is similar to def (which sets the default value).",
"Global variables are propagated to all templates that are rendered via the 'include' function.",
},
Func: func(name string, v interface{}) interface{} {
for here := t; here != nil; here = here.parent {
here.updateGlobal(name, v)
}
return ""
Func: func(n string, v interface{}) Void {
t.Global(n, v)
return voidValue
},
},
{
Expand All @@ -233,14 +262,7 @@ func (t *Template) DefaultFuncs() []Function {
"References / gets the variable named after the first argument.",
"The values must be set first by either def or global.",
},
Func: func(name string) interface{} {
if found, has := t.binds[name]; has {
return found
} else if v, has := t.defaults[name]; has {
return v.Value
}
return nil
},
Func: t.Ref,
},
{
Name: "q",
Expand Down
17 changes: 17 additions & 0 deletions pkg/template/funcs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ type testCloud struct {
ResourceList []interface{}
}

func TestDeepCopyObject(t *testing.T) {
resource := "disk"
input := testCloud{
Parameters: []testParameter{{ParameterKey: "foo", ParameterValue: "bar"}},
Resources: []testResource{{ResourceType: "test", ResourceTypePtr: &resource}},
}

copy, err := DeepCopyObject(input)
require.NoError(t, err)
require.Equal(t, input, copy)
inputStr, err := ToJSON(input)
require.NoError(t, err)
copyStr, err := ToJSON(copy)
require.NoError(t, err)
require.Equal(t, inputStr, copyStr)
}

func TestQueryObjectEncodeDecode(t *testing.T) {

param1 := testParameter{
Expand Down
39 changes: 39 additions & 0 deletions pkg/template/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,42 @@ func TestSourceAndGlobal(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "foo=100", view)
}

func TestIncludeAndGlobal(t *testing.T) {
r := `{{ global \"foo\" 100 }}` // the child template tries to mutate the global
s := `{{ include "str://` + r + `" }}foo={{ref "foo"}}`
tt, err := NewTemplate("str://"+s, Options{})
require.NoError(t, err)
tt.Global("foo", 200) // set the global of the calling / parent template
view, err := tt.Render(nil)
require.NoError(t, err)
require.Equal(t, "foo=200", view) // parent's not affected by child template
}

func TestSourceAndGlobalWithContext(t *testing.T) {
ctx := map[string]interface{}{
"a": 1,
"b": 2,
}
r := `{{ global \"foo\" 100 }}{{$void := set . \"a\" 100}}` // sourced mutates the context
s := `{{ source "str://` + r + `" }}a={{.a}}`
tt, err := NewTemplate("str://"+s, Options{})
require.NoError(t, err)
view, err := tt.Render(ctx)
require.NoError(t, err)
require.Equal(t, "a=100", view) // the sourced template mutated the calling template's context.
}

func TestIncludeAndGlobalWithContext(t *testing.T) {
ctx := map[string]interface{}{
"a": 1,
"b": 2,
}
r := `{{ global \"foo\" 100 }}{{$void := set . \"a\" 100}}` // included tries to mutate the context
s := `{{ include "str://` + r + `" }}a={{.a}}`
tt, err := NewTemplate("str://"+s, Options{})
require.NoError(t, err)
view, err := tt.Render(ctx)
require.NoError(t, err)
require.Equal(t, "a=1", view) // the included template cannot mutate the calling template's context.
}
57 changes: 53 additions & 4 deletions pkg/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,22 @@ type Template struct {
body []byte
parsed *template.Template
funcs map[string]interface{}
binds map[string]interface{}
globals map[string]interface{}
defaults map[string]defaultValue
context interface{}

registered []Function
lock sync.Mutex

parent *Template
}

// Void is used in the template functions return value type to indicate a void.
// Golang template does not allow functions with no return types to be bound.
type Void string

const voidValue Void = ""

// NewTemplate fetches the content at the url and returns a template. If the string begins
// with str:// as scheme, then the rest of the string is interpreted as the body of the template.
func NewTemplate(s string, opt Options) (*Template, error) {
Expand Down Expand Up @@ -111,7 +118,7 @@ func NewTemplateFromBytes(buff []byte, contextURL string, opt Options) (*Templat
url: contextURL,
body: buff,
funcs: map[string]interface{}{},
binds: map[string]interface{}{},
globals: map[string]interface{}{},
defaults: map[string]defaultValue{},
}, nil
}
Expand Down Expand Up @@ -145,10 +152,51 @@ func (t *Template) AddDef(name string, val interface{}, doc ...string) *Template
return t
}

// Ref returns the value keyed by name in the context of this template. See 'ref' template function.
func (t *Template) Ref(name string) interface{} {
if found, has := t.globals[name]; has {
return found
} else if v, has := t.defaults[name]; has {
return v.Value
}
return nil
}

// Dot returns the '.' in this template.
func (t *Template) Dot() interface{} {
return t.context
}

func (t *Template) forkFrom(parent *Template) (dotCopy interface{}, err error) {
t.lock.Lock()
defer t.lock.Unlock()

// copy the globals in the parent scope into the child
for k, v := range parent.globals {
t.globals[k] = v
}
// inherit the functions defined for this template
for k, v := range parent.funcs {
t.AddFunc(k, v)
}
if parent.context != nil {
return DeepCopyObject(parent.context)
}
return nil, nil
}

// Global sets the a key, value in the context of this template. It is visible to all the 'included'
// and 'sourced' templates by the calling template.
func (t *Template) Global(name string, value interface{}) {
for here := t; here != nil; here = here.parent {
here.updateGlobal(name, value)
}
}

func (t *Template) updateGlobal(name string, value interface{}) {
t.lock.Lock()
defer t.lock.Unlock()
t.binds[name] = value
t.globals[name] = value
}

// Validate parses the template and checks for validity.
Expand Down Expand Up @@ -220,6 +268,7 @@ func (t *Template) Execute(output io.Writer, context interface{}) error {
if err := t.build(toContext(context)); err != nil {
return err
}
t.context = context
return t.parsed.Execute(output, context)
}

Expand All @@ -240,7 +289,7 @@ func (t *Template) Render(context interface{}) (string, error) {
return "", err
}
var buff bytes.Buffer
err := t.parsed.Execute(&buff, context)
err := t.Execute(&buff, context)
return buff.String(), err
}

Expand Down

0 comments on commit b919e4f

Please sign in to comment.