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

Commit b919e4f

Browse files
author
David Chung
authored
Template improvements (#390)
Signed-off-by: David Chung <[email protected]>
1 parent 908e9fb commit b919e4f

File tree

6 files changed

+171
-44
lines changed

6 files changed

+171
-44
lines changed

examples/flavor/swarm/flavor.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func (s *baseFlavor) prepare(role string, flavorProperties *types.Any, instanceS
160160
log.Warningln("Worker prepare:", err)
161161
}
162162

163-
swarmID := "?"
163+
swarmID = "?"
164164
if swarmStatus != nil {
165165
swarmID = swarmStatus.ID
166166
}
@@ -342,7 +342,7 @@ func (c *templateContext) Funcs() []template.Function {
342342
},
343343
},
344344
{
345-
Name: "SWARM_MANAGER_IP",
345+
Name: "SWARM_MANAGER_ADDR",
346346
Description: []string{"IP of the Swarm manager / leader"},
347347
Func: func() (string, error) {
348348
if c.nodeInfo == nil {

examples/flavor/swarm/templates.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ EOF
2020
kill -s HUP $(cat /var/run/docker.pid)
2121
sleep 5
2222
23-
{{ if eq INSTANCE_LOGICAL_ID SPEC.SwarmJoinIP }}
23+
{{ if and ( eq INSTANCE_LOGICAL_ID SPEC.SwarmJoinIP ) (not SWARM_INITIALIZED) }}
2424
2525
{{/* The first node of the special allocations will initialize the swarm. */}}
2626
docker swarm init --advertise-addr {{ INSTANCE_LOGICAL_ID }}
@@ -34,7 +34,7 @@ sleep 5
3434
{{ else }}
3535
3636
{{/* The rest of the nodes will join as followers in the manager group. */}}
37-
docker swarm join --token {{ SWARM_JOIN_TOKENS.Manager }} {{ SPEC.SwarmJoinIP }}:2377
37+
docker swarm join --token {{ SWARM_JOIN_TOKENS.Manager }} {{ SWARM_MANAGER_ADDR }}
3838
3939
{{ end }}
4040
`
@@ -59,7 +59,7 @@ kill -s HUP $(cat /var/run/docker.pid)
5959
6060
sleep 5
6161
62-
docker swarm join --token {{ SWARM_JOIN_TOKENS.Worker }} {{ SPEC.SwarmJoinIP }}:2377
62+
docker swarm join --token {{ SWARM_JOIN_TOKENS.Worker }} {{ SWARM_MANAGER_ADDR }}
6363
6464
`
6565
)

pkg/template/funcs.go

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package template
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
67
"reflect"
@@ -10,6 +11,24 @@ import (
1011
"github.com/jmespath/go-jmespath"
1112
)
1213

14+
// DeepCopyObject makes a deep copy of the argument, using encoding/gob encode/decode.
15+
func DeepCopyObject(from interface{}) (interface{}, error) {
16+
var mod bytes.Buffer
17+
enc := json.NewEncoder(&mod)
18+
dec := json.NewDecoder(&mod)
19+
err := enc.Encode(from)
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
copy := reflect.New(reflect.TypeOf(from))
25+
err = dec.Decode(copy.Interface())
26+
if err != nil {
27+
return nil, err
28+
}
29+
return reflect.Indirect(copy).Interface(), nil
30+
}
31+
1332
// QueryObject applies a JMESPath query specified by the expression, against the target object.
1433
func QueryObject(exp string, target interface{}) (interface{}, error) {
1534
query, err := jmespath.Compile(exp)
@@ -119,8 +138,14 @@ func (t *Template) DefaultFuncs() []Function {
119138
Description: []string{
120139
"Source / evaluate the template at the input location (as URL).",
121140
"This will make all of the global variables declared there visible in this template's context.",
141+
"Similar to 'source' in bash, sourcing another template means applying it in the same context ",
142+
"as the calling template. The context (e.g. variables) of the calling template as a result can be mutated.",
122143
},
123-
Func: func(p string) (string, error) {
144+
Func: func(p string, opt ...interface{}) (string, error) {
145+
var o interface{}
146+
if len(opt) > 0 {
147+
o = opt[0]
148+
}
124149
loc := p
125150
if strings.Index(loc, "str://") == -1 {
126151
buff, err := getURL(t.url, p)
@@ -133,48 +158,54 @@ func (t *Template) DefaultFuncs() []Function {
133158
if err != nil {
134159
return "", err
135160
}
136-
// copy the binds in the parent scope into the child
137-
for k, v := range t.binds {
138-
sourced.binds[k] = v
139-
}
140-
// inherit the functions defined for this template
141-
for k, v := range t.funcs {
142-
sourced.AddFunc(k, v)
143-
}
144161
// set this as the parent of the sourced template so its global can mutate the globals in this
145162
sourced.parent = t
163+
sourced.forkFrom(t)
164+
sourced.context = t.context
146165

166+
if o == nil {
167+
o = sourced.context
168+
}
147169
// TODO(chungers) -- let the sourced template define new functions that can be called in the parent.
148-
return sourced.Render(nil)
170+
return sourced.Render(o)
149171
},
150172
},
151173
{
152174
Name: "include",
153175
Description: []string{
154176
"Render content found at URL as template and include here.",
155177
"The optional second parameter is the context to use when rendering the template.",
178+
"Conceptually similar to exec in bash, where the template included is applied using a fork ",
179+
"of current context in the calling template. Any mutations to the context via 'global' will not ",
180+
"be visible in the calling template's context.",
156181
},
157182
Func: func(p string, opt ...interface{}) (string, error) {
158183
var o interface{}
159184
if len(opt) > 0 {
160185
o = opt[0]
161186
}
162-
loc, err := getURL(t.url, p)
163-
if err != nil {
164-
return "", err
187+
loc := p
188+
if strings.Index(loc, "str://") == -1 {
189+
buff, err := getURL(t.url, p)
190+
if err != nil {
191+
return "", err
192+
}
193+
loc = buff
165194
}
166195
included, err := NewTemplate(loc, t.options)
167196
if err != nil {
168197
return "", err
169198
}
170-
// copy the binds in the parent scope into the child
171-
for k, v := range t.binds {
172-
included.binds[k] = v
199+
dotCopy, err := included.forkFrom(t)
200+
if err != nil {
201+
return "", err
173202
}
174-
// inherit the functions defined for this template
175-
for k, v := range t.funcs {
176-
included.AddFunc(k, v)
203+
included.context = dotCopy
204+
205+
if o == nil {
206+
o = included.context
177207
}
208+
178209
return included.Render(o)
179210
},
180211
},
@@ -193,10 +224,10 @@ func (t *Template) DefaultFuncs() []Function {
193224
"Defines a variable with the first argument as name and last argument value as the default.",
194225
"It's also ok to pass a third optional parameter, in the middle, as the documentation string.",
195226
},
196-
Func: func(name string, args ...interface{}) (string, error) {
227+
Func: func(name string, args ...interface{}) (Void, error) {
197228
if _, has := t.defaults[name]; has {
198229
// not sure if this is good, but should complain loudly
199-
return "", fmt.Errorf("already defined: %v", name)
230+
return voidValue, fmt.Errorf("already defined: %v", name)
200231
}
201232
var doc string
202233
var value interface{}
@@ -210,7 +241,7 @@ func (t *Template) DefaultFuncs() []Function {
210241
value = args[1]
211242
}
212243
t.AddDef(name, value, doc)
213-
return "", nil
244+
return voidValue, nil
214245
},
215246
},
216247
{
@@ -220,11 +251,9 @@ func (t *Template) DefaultFuncs() []Function {
220251
"This is similar to def (which sets the default value).",
221252
"Global variables are propagated to all templates that are rendered via the 'include' function.",
222253
},
223-
Func: func(name string, v interface{}) interface{} {
224-
for here := t; here != nil; here = here.parent {
225-
here.updateGlobal(name, v)
226-
}
227-
return ""
254+
Func: func(n string, v interface{}) Void {
255+
t.Global(n, v)
256+
return voidValue
228257
},
229258
},
230259
{
@@ -233,14 +262,7 @@ func (t *Template) DefaultFuncs() []Function {
233262
"References / gets the variable named after the first argument.",
234263
"The values must be set first by either def or global.",
235264
},
236-
Func: func(name string) interface{} {
237-
if found, has := t.binds[name]; has {
238-
return found
239-
} else if v, has := t.defaults[name]; has {
240-
return v.Value
241-
}
242-
return nil
243-
},
265+
Func: t.Ref,
244266
},
245267
{
246268
Name: "q",

pkg/template/funcs_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,23 @@ type testCloud struct {
2525
ResourceList []interface{}
2626
}
2727

28+
func TestDeepCopyObject(t *testing.T) {
29+
resource := "disk"
30+
input := testCloud{
31+
Parameters: []testParameter{{ParameterKey: "foo", ParameterValue: "bar"}},
32+
Resources: []testResource{{ResourceType: "test", ResourceTypePtr: &resource}},
33+
}
34+
35+
copy, err := DeepCopyObject(input)
36+
require.NoError(t, err)
37+
require.Equal(t, input, copy)
38+
inputStr, err := ToJSON(input)
39+
require.NoError(t, err)
40+
copyStr, err := ToJSON(copy)
41+
require.NoError(t, err)
42+
require.Equal(t, inputStr, copyStr)
43+
}
44+
2845
func TestQueryObjectEncodeDecode(t *testing.T) {
2946

3047
param1 := testParameter{

pkg/template/integration_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,42 @@ func TestSourceAndGlobal(t *testing.T) {
179179
require.NoError(t, err)
180180
require.Equal(t, "foo=100", view)
181181
}
182+
183+
func TestIncludeAndGlobal(t *testing.T) {
184+
r := `{{ global \"foo\" 100 }}` // the child template tries to mutate the global
185+
s := `{{ include "str://` + r + `" }}foo={{ref "foo"}}`
186+
tt, err := NewTemplate("str://"+s, Options{})
187+
require.NoError(t, err)
188+
tt.Global("foo", 200) // set the global of the calling / parent template
189+
view, err := tt.Render(nil)
190+
require.NoError(t, err)
191+
require.Equal(t, "foo=200", view) // parent's not affected by child template
192+
}
193+
194+
func TestSourceAndGlobalWithContext(t *testing.T) {
195+
ctx := map[string]interface{}{
196+
"a": 1,
197+
"b": 2,
198+
}
199+
r := `{{ global \"foo\" 100 }}{{$void := set . \"a\" 100}}` // sourced mutates the context
200+
s := `{{ source "str://` + r + `" }}a={{.a}}`
201+
tt, err := NewTemplate("str://"+s, Options{})
202+
require.NoError(t, err)
203+
view, err := tt.Render(ctx)
204+
require.NoError(t, err)
205+
require.Equal(t, "a=100", view) // the sourced template mutated the calling template's context.
206+
}
207+
208+
func TestIncludeAndGlobalWithContext(t *testing.T) {
209+
ctx := map[string]interface{}{
210+
"a": 1,
211+
"b": 2,
212+
}
213+
r := `{{ global \"foo\" 100 }}{{$void := set . \"a\" 100}}` // included tries to mutate the context
214+
s := `{{ include "str://` + r + `" }}a={{.a}}`
215+
tt, err := NewTemplate("str://"+s, Options{})
216+
require.NoError(t, err)
217+
view, err := tt.Render(ctx)
218+
require.NoError(t, err)
219+
require.Equal(t, "a=1", view) // the included template cannot mutate the calling template's context.
220+
}

pkg/template/template.go

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,22 @@ type Template struct {
7171
body []byte
7272
parsed *template.Template
7373
funcs map[string]interface{}
74-
binds map[string]interface{}
74+
globals map[string]interface{}
7575
defaults map[string]defaultValue
76+
context interface{}
7677

7778
registered []Function
7879
lock sync.Mutex
7980

8081
parent *Template
8182
}
8283

84+
// Void is used in the template functions return value type to indicate a void.
85+
// Golang template does not allow functions with no return types to be bound.
86+
type Void string
87+
88+
const voidValue Void = ""
89+
8390
// NewTemplate fetches the content at the url and returns a template. If the string begins
8491
// with str:// as scheme, then the rest of the string is interpreted as the body of the template.
8592
func NewTemplate(s string, opt Options) (*Template, error) {
@@ -111,7 +118,7 @@ func NewTemplateFromBytes(buff []byte, contextURL string, opt Options) (*Templat
111118
url: contextURL,
112119
body: buff,
113120
funcs: map[string]interface{}{},
114-
binds: map[string]interface{}{},
121+
globals: map[string]interface{}{},
115122
defaults: map[string]defaultValue{},
116123
}, nil
117124
}
@@ -145,10 +152,51 @@ func (t *Template) AddDef(name string, val interface{}, doc ...string) *Template
145152
return t
146153
}
147154

155+
// Ref returns the value keyed by name in the context of this template. See 'ref' template function.
156+
func (t *Template) Ref(name string) interface{} {
157+
if found, has := t.globals[name]; has {
158+
return found
159+
} else if v, has := t.defaults[name]; has {
160+
return v.Value
161+
}
162+
return nil
163+
}
164+
165+
// Dot returns the '.' in this template.
166+
func (t *Template) Dot() interface{} {
167+
return t.context
168+
}
169+
170+
func (t *Template) forkFrom(parent *Template) (dotCopy interface{}, err error) {
171+
t.lock.Lock()
172+
defer t.lock.Unlock()
173+
174+
// copy the globals in the parent scope into the child
175+
for k, v := range parent.globals {
176+
t.globals[k] = v
177+
}
178+
// inherit the functions defined for this template
179+
for k, v := range parent.funcs {
180+
t.AddFunc(k, v)
181+
}
182+
if parent.context != nil {
183+
return DeepCopyObject(parent.context)
184+
}
185+
return nil, nil
186+
}
187+
188+
// Global sets the a key, value in the context of this template. It is visible to all the 'included'
189+
// and 'sourced' templates by the calling template.
190+
func (t *Template) Global(name string, value interface{}) {
191+
for here := t; here != nil; here = here.parent {
192+
here.updateGlobal(name, value)
193+
}
194+
}
195+
148196
func (t *Template) updateGlobal(name string, value interface{}) {
149197
t.lock.Lock()
150198
defer t.lock.Unlock()
151-
t.binds[name] = value
199+
t.globals[name] = value
152200
}
153201

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

@@ -240,7 +289,7 @@ func (t *Template) Render(context interface{}) (string, error) {
240289
return "", err
241290
}
242291
var buff bytes.Buffer
243-
err := t.parsed.Execute(&buff, context)
292+
err := t.Execute(&buff, context)
244293
return buff.String(), err
245294
}
246295

0 commit comments

Comments
 (0)