forked from hashicorp/consul
/
template.go
204 lines (179 loc) · 5.57 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
package prepared_query
import (
"fmt"
"reflect"
"regexp"
"strings"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/mitchellh/copystructure"
)
// IsTemplate returns true if the given query is a template.
func IsTemplate(query *structs.PreparedQuery) bool {
return query.Template.Type != ""
}
// CompiledTemplate is an opaque object that can be used later to render a
// prepared query template.
type CompiledTemplate struct {
// query keeps a copy of the original query for rendering.
query *structs.PreparedQuery
// trees contains a map with paths to string fields in a structure to
// parsed syntax trees, suitable for later evaluation.
trees map[string]ast.Node
// re is the compiled regexp, if they supplied one (this can be nil).
re *regexp.Regexp
// removeEmptyTags will cause the service tags to be stripped of any
// empty strings after interpolation.
removeEmptyTags bool
}
// Compile validates a prepared query template and returns an opaque compiled
// object that can be used later to render the template.
func Compile(query *structs.PreparedQuery) (*CompiledTemplate, error) {
// Make sure it's a type we understand.
if query.Template.Type != structs.QueryTemplateTypeNamePrefixMatch {
return nil, fmt.Errorf("Bad Template.Type '%s'", query.Template.Type)
}
// Start compile.
ct := &CompiledTemplate{
trees: make(map[string]ast.Node),
removeEmptyTags: query.Template.RemoveEmptyTags,
}
// Make a copy of the query to use as the basis for rendering later.
dup, err := copystructure.Copy(query)
if err != nil {
return nil, err
}
var ok bool
ct.query, ok = dup.(*structs.PreparedQuery)
if !ok {
return nil, fmt.Errorf("Failed to copy query")
}
// Walk over all the string fields in the Service sub-structure and
// parse them as HIL.
parse := func(path string, v reflect.Value) error {
tree, err := hil.Parse(v.String())
if err != nil {
return fmt.Errorf("Bad format '%s' in Service%s: %s", v.String(), path, err)
}
ct.trees[path] = tree
return nil
}
if err := walk(&ct.query.Service, parse); err != nil {
return nil, err
}
// If they supplied a regexp then compile it.
if ct.query.Template.Regexp != "" {
var err error
ct.re, err = regexp.Compile(ct.query.Template.Regexp)
if err != nil {
return nil, fmt.Errorf("Bad Regexp: %s", err)
}
}
// Finally do a test render with the supplied name prefix. This will
// help catch errors before run time, and this is the most minimal
// prefix it will be expected to run with. The results might not make
// sense and create a valid service to lookup, but it should render
// without any errors.
if _, err = ct.Render(ct.query.Name, structs.QuerySource{}); err != nil {
return nil, err
}
return ct, nil
}
// Render takes a compiled template and renders it for the given name. For
// example, if the user looks up foobar.query.consul via DNS then we will call
// this function with "foobar" on the compiled template.
func (ct *CompiledTemplate) Render(name string, source structs.QuerySource) (*structs.PreparedQuery, error) {
// Make it "safe" to render a default structure.
if ct == nil {
return nil, fmt.Errorf("Cannot render an uncompiled template")
}
// Start with a fresh, detached copy of the original so we don't disturb
// the prototype.
dup, err := copystructure.Copy(ct.query)
if err != nil {
return nil, err
}
query, ok := dup.(*structs.PreparedQuery)
if !ok {
return nil, fmt.Errorf("Failed to copy query")
}
// Run the regular expression, if provided. We execute on a copy here
// to avoid internal lock contention because we expect this to be called
// from multiple goroutines.
var matches []string
if ct.re != nil {
re := ct.re.Copy()
matches = re.FindStringSubmatch(name)
}
// Create a safe match function that can't fail at run time. It will
// return an empty string for any invalid input.
match := ast.Function{
ArgTypes: []ast.Type{ast.TypeInt},
ReturnType: ast.TypeString,
Variadic: false,
Callback: func(inputs []interface{}) (interface{}, error) {
i, ok := inputs[0].(int)
if ok && i >= 0 && i < len(matches) {
return matches[i], nil
}
return "", nil
},
}
// Build up the HIL evaluation context.
config := &hil.EvalConfig{
GlobalScope: &ast.BasicScope{
VarMap: map[string]ast.Variable{
"name.full": ast.Variable{
Type: ast.TypeString,
Value: name,
},
"name.prefix": ast.Variable{
Type: ast.TypeString,
Value: query.Name,
},
"name.suffix": ast.Variable{
Type: ast.TypeString,
Value: strings.TrimPrefix(name, query.Name),
},
"agent.segment": ast.Variable{
Type: ast.TypeString,
Value: source.Segment,
},
},
FuncMap: map[string]ast.Function{
"match": match,
},
},
}
// Run through the Service sub-structure and evaluate all the strings
// as HIL.
eval := func(path string, v reflect.Value) error {
tree, ok := ct.trees[path]
if !ok {
return nil
}
res, err := hil.Eval(tree, config)
if err != nil {
return fmt.Errorf("Bad evaluation for '%s' in Service%s: %s", v.String(), path, err)
}
if res.Type != hil.TypeString {
return fmt.Errorf("Expected Service%s field to be a string, got %s", path, res.Type)
}
v.SetString(res.Value.(string))
return nil
}
if err := walk(&query.Service, eval); err != nil {
return nil, err
}
if ct.removeEmptyTags {
tags := make([]string, 0, len(query.Service.Tags))
for _, tag := range query.Service.Tags {
if tag != "" {
tags = append(tags, tag)
}
}
query.Service.Tags = tags
}
return query, nil
}