-
Notifications
You must be signed in to change notification settings - Fork 195
/
intrinsics.go
262 lines (216 loc) · 8.42 KB
/
intrinsics.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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package intrinsics
import (
"encoding/base64"
"encoding/json"
"fmt"
yamlwrapper "github.com/sanathkr/yaml"
)
// IntrinsicHandler is a function that applies an intrinsic function and returns
// the response that should be placed in it's place. An intrinsic handler function
// is passed the name of the intrinsic function (e.g. Fn::Join), and the object
// to apply it to (as an interface{}), and should return the resolved object (as an interface{}).
type IntrinsicHandler func(string, interface{}, interface{}) interface{}
// IntrinsicFunctionHandlers is a map of all the possible AWS CloudFormation intrinsic
// functions, and a handler function that is invoked to resolve.
var defaultIntrinsicHandlers = map[string]IntrinsicHandler{
"Fn::Base64": FnBase64,
"Fn::And": FnAnd,
"Fn::Equals": FnEquals,
"Fn::If": FnIf,
"Fn::Not": FnNot,
"Fn::Or": FnOr,
"Fn::FindInMap": FnFindInMap,
"Fn::GetAtt": nonResolvingHandler,
"Fn::GetAZs": FnGetAZs,
"Fn::ImportValue": nonResolvingHandler,
"Fn::Join": FnJoin,
"Fn::Select": FnSelect,
"Fn::Split": FnSplit,
"Fn::Sub": FnSub,
"Ref": Ref,
"Fn::Cidr": nonResolvingHandler,
"Fn::Transform": FnTransform,
}
// ProcessorOptions allows customisation of the intrinsic function processor behaviour.
// This allows disabling the processing of intrinsics,
// overriding of the handlers for each intrinsic function type,
// and overriding template parameters.
type ProcessorOptions struct {
IntrinsicHandlerOverrides map[string]IntrinsicHandler
ParameterOverrides map[string]interface{}
NoProcess bool
EvaluateConditions bool
}
// nonResolvingHandler is a simple example of an intrinsic function handler function
// that refuses to resolve any intrinsic functions, and just returns a basic string.
func nonResolvingHandler(name string, input interface{}, template interface{}) interface{} {
return nil
}
// ProcessYAML recursively searches through a byte array of JSON data for all
// AWS CloudFormation intrinsic functions, resolves them, and then returns
// the resulting interface{} object.
func ProcessYAML(input []byte, options *ProcessorOptions) ([]byte, error) {
// Convert short form intrinsic functions (e.g. !Sub) to long form
registerTagMarshallers()
data, err := yamlwrapper.YAMLToJSON(input)
if err != nil {
return nil, fmt.Errorf("invalid YAML template: %s", err)
}
return ProcessJSON(data, options)
}
// ProcessJSON recursively searches through a byte array of JSON data for all
// AWS CloudFormation intrinsic functions, resolves them, and then returns
// the resulting interface{} object.
func ProcessJSON(input []byte, options *ProcessorOptions) ([]byte, error) {
// First, unmarshal the JSON to a generic interface{} type
var unmarshalled interface{}
if err := json.Unmarshal(input, &unmarshalled); err != nil {
return nil, fmt.Errorf("invalid JSON: %s", err)
}
var processed interface{}
if options != nil && options.NoProcess {
processed = unmarshalled
} else {
overrideParameters(unmarshalled, options)
if options != nil && options.EvaluateConditions {
evaluateConditions(unmarshalled, options)
}
// Process all of the intrinsic functions
processed = search(unmarshalled, unmarshalled, options)
}
// And return the result back as a []byte of JSON
result, err := json.MarshalIndent(processed, "", " ")
if err != nil {
return nil, fmt.Errorf("invalid JSON: %s", err)
}
return result, nil
}
// overrideParameters replaces the default values of Parameters with the specified ones
func overrideParameters(input interface{}, options *ProcessorOptions) {
if options == nil || len(options.ParameterOverrides) == 0 {
return
}
// Check the template is a map
if template, ok := input.(map[string]interface{}); ok {
// Check there is a parameters section
if uparameters, ok := template["Parameters"]; ok {
// Check the parameters section is a map
if parameters, ok := uparameters.(map[string]interface{}); ok {
for name, value := range options.ParameterOverrides {
// Check there is a parameter with the same name as the Ref
if uparameter, ok := parameters[name]; ok {
// Check the parameter is a map
if parameter, ok := uparameter.(map[string]interface{}); ok {
// Set the default value
parameter["Default"] = value
}
}
}
}
}
}
}
// evaluateConditions replaces each condition in the template with its corresponding
// value
func evaluateConditions(input interface{}, options *ProcessorOptions) {
if template, ok := input.(map[string]interface{}); ok {
// Check there is a conditions section
if uconditions, ok := template["Conditions"]; ok {
// Check the conditions section is a map
if conditions, ok := uconditions.(map[string]interface{}); ok {
for name, expr := range conditions {
conditions[name] = search(expr, input, options)
}
}
}
}
}
// Search is a recursive function, that will search through an interface{} looking for
// an intrinsic function. If it finds one, it calls the provided handler function, passing
// it the type of intrinsic function (e.g. 'Fn::Join'), and the contents. The intrinsic
// handler is expected to return the value that is supposed to be there.
func search(input interface{}, template interface{}, options *ProcessorOptions) interface{} {
switch value := input.(type) {
case map[string]interface{}:
// We've found an object in the JSON, it might be an intrinsic, it might not.
// To check, we need to see if it contains a specific key that matches the name
// of an intrinsic function. As golang maps do not guarentee ordering, we need
// to check every key, not just the first.
processed := map[string]interface{}{}
for key, val := range value {
// See if we have an intrinsic handler function for this object key provided in the
if h, ok := handler(key, options); ok {
// This is an intrinsic function, so replace the intrinsic function object
// with the result of calling the intrinsic function handler for this type
return h(key, search(val, template, options), template)
}
if key == "Condition" && (options != nil && options.EvaluateConditions) {
// This can lead to infinite recursion A -> B; B -> A;
// pass state of the conditions that we're evaluating so we can detect cycles
// in case of cycle or not found, do nothing
if con := condition(key, search(val, template, options), template, options); con != nil {
return con
}
}
// This is not an intrinsic function, recurse through it normally
processed[key] = search(val, template, options)
}
return processed
case []interface{}:
// We found an array in the JSON - recurse through it's elements looking for intrinsic functions
processed := []interface{}{}
for _, val := range value {
processed = append(processed, search(val, template, options))
}
return processed
case nil:
return value
case bool:
return value
case float64:
return value
case string:
// Check if the string can be unmarshalled into an intrinsic object
var decoded []byte
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil {
// The string value is not base64 encoded, so it's not an intrinsic so just pass it back
return value
}
var intrinsic map[string]interface{}
if err := json.Unmarshal([]byte(decoded), &intrinsic); err != nil {
// The string value is not JSON, so it's not an intrinsic so just pass it back
return value
}
// An intrinsic should be an object, with a single key containing a valid intrinsic name
if len(intrinsic) != 1 {
return value
}
for key, val := range intrinsic {
// See if this is a valid intrinsic function, by comparing the name with our list of registered handlers
if _, ok := handler(key, options); ok {
return map[string]interface{}{
key: search(val, template, options),
}
}
}
return value
default:
return nil
}
}
// handler looks up the correct intrinsic function handler for an object key, if there is one.
// If not, it returns nil, false.
func handler(name string, options *ProcessorOptions) (IntrinsicHandler, bool) {
// Check if we have a handler for this intrinsic type in the instrinsic handler
// overrides in the options provided to Process()
if options != nil {
if h, ok := options.IntrinsicHandlerOverrides[name]; ok {
return h, true
}
}
if h, ok := defaultIntrinsicHandlers[name]; ok {
return h, true
}
return nil, false
}