-
Notifications
You must be signed in to change notification settings - Fork 12
/
template.go
225 lines (185 loc) · 6.2 KB
/
template.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package hcat
import (
"bytes"
"crypto/md5"
"encoding/hex"
"sync"
"sync/atomic"
"text/template"
"github.com/hashicorp/hcat/dep"
"github.com/pkg/errors"
)
// ErrMissingValues is the error returned when a template doesn't completely
// render due to missing values (values that haven't been fetched yet).
var ErrMissingValues = errors.New("missing template values")
var ErrNoNewValues = errors.New("no new values for template")
// Template is the internal representation of an individual template to process.
// The template retains the relationship between it's contents and is
// responsible for it's own execution.
type Template struct {
// template name, appened to ID (random if not specified)
name string
// contents is the string contents for the template. It is either given
// during template creation or read from disk when initialized.
contents string
// dirty indicates that the template's data has been updated and it
// needs to be re-rendered
dirty drainableChan
// leftDelim and rightDelim are the template delimiters.
leftDelim string
rightDelim string
// hexMD5 stores the hex version of the MD5
hexMD5 string
// errMissingKey causes the template processing to exit immediately if a map
// is indexed with a key that does not exist.
errMissingKey bool
// FuncMapMerge a map of functions that add-to or override
// those used when executing the template. (text/template)
funcMapMerge template.FuncMap
// sandboxPath adds a prefix to any path provided to the `file` function
// and causes an error if a relative path tries to traverse outside that
// prefix.
sandboxPath string
// Renderer is the default renderer used for this template
renderer Renderer
// cache for the current rendered template content
cache atomic.Value
once sync.Once // for cache init
}
// Renderer defines the interface used to render (output) and template.
// FileRenderer implements this to write to disk.
type Renderer interface {
Render(contents []byte) (RenderResult, error)
}
// Recaller is the read interface for the cache
// Implemented by Store and Watcher (which wraps Store)
type Recaller func(dep.Dependency) (value interface{}, found bool)
// TemplateInput is used as input when creating the template.
type TemplateInput struct {
// Optional name for the template. Appended to the ID. Required if you want
// to use the same content in more than one template with the same Watcher.
Name string
// Contents are the raw template contents.
Contents string
// ErrMissingKey causes the template parser to exit immediately with an
// error when a map is indexed with a key that does not exist.
ErrMissingKey bool
// LeftDelim and RightDelim are the template delimiters.
LeftDelim string
RightDelim string
// FuncMapMerge a map of functions that add-to or override those used when
// executing the template. (text/template)
//
// There is a special case for FuncMapMerge where, if matched, gets
// called with the cache, used and missing sets (like the dependency
// functions) should return a function that matches a signature required
// by text/template's Funcmap (masked by an interface).
// This special case function's signature should match:
// func(Recaller) interface{}
FuncMapMerge template.FuncMap
// SandboxPath adds a prefix to any path provided to the `file` function
// and causes an error if a relative path tries to traverse outside that
// prefix.
SandboxPath string
// Renderer is the default renderer used for this template
Renderer Renderer
}
// NewTemplate creates a new Template and primes it for the initial run.
func NewTemplate(i TemplateInput) *Template {
var t Template
t.name = i.Name
t.contents = i.Contents
t.leftDelim = i.LeftDelim
t.rightDelim = i.RightDelim
t.errMissingKey = i.ErrMissingKey
t.sandboxPath = i.SandboxPath
t.funcMapMerge = i.FuncMapMerge
t.renderer = i.Renderer
t.dirty = make(drainableChan, 1)
t.Notify(nil) // prime template as needing to be run
// Compute the MD5, encode as hex
hash := md5.Sum([]byte(t.contents))
t.hexMD5 = hex.EncodeToString(hash[:])
return &t
}
// ID returns the identifier for this template.
// Used to uniquely identify this template object for dependency management.
func (t *Template) ID() string {
if t.name != "" {
return t.hexMD5 + "_" + t.name
}
return t.hexMD5
}
// Notify template that a dependency it relies on has been updated. Works by
// marking the template so it knows it has new data to process when Execute is
// called.
func (t *Template) Notify(interface{}) bool {
select {
case t.dirty <- struct{}{}:
default:
}
return true
}
// Check and clear dirty flag
func (t *Template) isDirty() bool {
select {
case <-t.dirty:
return true
default:
return false
}
}
// Render calls the stored Renderer with the passed content
func (t *Template) Render(content []byte) (RenderResult, error) {
return t.renderer.Render(content)
}
// Execute evaluates this template in the provided context.
func (t *Template) Execute(rec Recaller) ([]byte, error) {
t.once.Do(func() { t.cache.Store([]byte{}) }) // init cache
if !t.isDirty() {
return t.cache.Load().([]byte), ErrNoNewValues
}
tmpl := template.New(t.ID())
tmpl.Delims(t.leftDelim, t.rightDelim)
tmpl.Funcs(funcMap(&funcMapInput{
recaller: rec,
funcMapMerge: t.funcMapMerge,
}))
if t.errMissingKey {
tmpl.Option("missingkey=error")
} else {
tmpl.Option("missingkey=zero")
}
tmpl, err := tmpl.Parse(t.contents)
if err != nil {
return nil, errors.Wrap(err, "parse")
}
// Execute the template into the writer
var b bytes.Buffer
if err := tmpl.Execute(&b, nil); err != nil {
return nil, errors.Wrap(err, "execute")
}
content := b.Bytes()
t.cache.Store(content)
return content, nil
}
// funcMapInput is input to the funcMap, which builds the template functions.
type funcMapInput struct {
recaller Recaller
funcMapMerge template.FuncMap
}
// funcMap is the map of template functions to their respective functions.
func funcMap(i *funcMapInput) template.FuncMap {
r := make(template.FuncMap, len(i.funcMapMerge))
for k, v := range i.funcMapMerge {
switch f := v.(type) {
case func(Recaller) interface{}:
r[k] = f(i.recaller)
default:
r[k] = v
}
}
return r
}