-
Notifications
You must be signed in to change notification settings - Fork 22
/
index.js
349 lines (311 loc) · 12.8 KB
/
index.js
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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
const Https = require('https')
const Url = require('url')
var ValidationCheck = require('./src/validationCheck')
var SDKAlias = require('./src/SDKAlias')
var JSONDeepEquals = require('./src/JSONDeepEquals')
var DefaultExpander = require('./src/DefaultExpander')
var Composite = require('./src/Composite')
const PluckedEquality = (keySet, fresh, old) => JSONDeepEquals(pluck(keySet, fresh), pluck(keySet, old))
const pluck = (keySet, hash) => keySet.reduce((plucked, key) => ({ ...plucked, [key]: hash[key] }), {})
function ReplyAfterHandler(promise, reply) {
promise.then(function(response) {
var { PhysicalResourceId, FnGetAttrsDataObj } = response || {}
reply(null, PhysicalResourceId, FnGetAttrsDataObj)
}).catch(function(err) {
reply(err.message || 'Unknown error')
})
}
const getEnvironment = ({ invokedFunctionArn }) => {
const [LambdaArn, Region, AccountId, LambdaName] = invokedFunctionArn.match(/^arn:aws.*:lambda:(\w+-\w+-\d+):(\d+):function:(.*)$/)
return {
LambdaArn,
Region,
AccountId,
LambdaName
}
}
function CfnLambdaFactory(resourceDefinition) {
return function CfnLambda(event, context) {
// support Async handler functions:
if(resourceDefinition.AsyncCreate) {
if(resourceDefinition.Create) {
console.log('WARNING: Both Create and AsyncCreate handlers defined. Ignoring AsyncCreate')
} else {
resourceDefinition.Create = function(CfnRequestParams, reply) {
return ReplyAfterHandler(
resourceDefinition.AsyncCreate(CfnRequestParams), reply
)
}
}
}
if(resourceDefinition.AsyncUpdate) {
if(resourceDefinition.Update) {
console.log('WARNING: Both Update and AsyncUpdate handlers defined. Ignoring AsyncUpdate')
} else {
resourceDefinition.Update = function(RequestPhysicalID, CfnRequestParams, OldCfnRequestParams, reply) {
return ReplyAfterHandler(
resourceDefinition.AsyncUpdate(RequestPhysicalID, CfnRequestParams, OldCfnRequestParams), reply
)
}
}
}
if(resourceDefinition.AsyncDelete) {
if(resourceDefinition.Delete) {
console.log('WARNING: Both Delete and AsyncDelete handlers defined. Ignoring AsyncDelete')
} else {
resourceDefinition.Delete = function(RequestPhysicalID, CfnRequestParams, reply) {
return ReplyAfterHandler(
resourceDefinition.AsyncDelete(RequestPhysicalID, CfnRequestParams), reply
)
}
}
}
if(resourceDefinition.AsyncNoUpdate) {
if(resourceDefinition.NoUpdate) {
console.log('WARNING: Both NoUpdate and AsyncNoUpdate handlers defined. Ignoring AsyncNoUpdate')
} else {
resourceDefinition.NoUpdate = function(PhysicalResourceId, CfnResourceProperties, reply) {
return ReplyAfterHandler(
resourceDefinition.AsyncNoUpdate(PhysicalResourceId, CfnResourceProperties), reply
)
}
}
}
if (event && event.ResourceProperties) {
delete event.ResourceProperties.ServiceToken
}
if (event && event.OldResourceProperties) {
delete event.OldResourceProperties.ServiceToken
}
CfnLambdaFactory.Environment = getEnvironment(context)
var RequestType = event.RequestType
var Params = event.ResourceProperties &&
DefaultExpander(event.ResourceProperties)
var OldParams = event.OldResourceProperties &&
DefaultExpander(event.OldResourceProperties)
var RequestPhysicalId = event.PhysicalResourceId
var NormalReply = replyWithFunctor(sendResponse)
console.log('REQUEST RECEIVED:\n', JSON.stringify(event))
function replyOrLongRunning() {
var longRunningConf = resourceDefinition.LongRunning
console.log('Checking for long running configs...')
if (longRunningConf &&
longRunningConf.PingInSeconds &&
longRunningConf.MaxPings &&
longRunningConf.LambdaApi &&
longRunningConf.Methods &&
'function' === typeof longRunningConf.Methods[RequestType]) {
console.log('Long running configurations found, ' +
'providing this callback instead of the normal reply ' +
'to CloudFormation for action %s.', RequestType)
console.log('LongRunning configs: %j', longRunningConf)
return replyWithFunctor(triggerLongRunningReply)
}
console.log('Did not find valid LongRunning configs, ' +
'proceed as normal req: %j', longRunningConf)
return NormalReply
}
if (event.LongRunningRequestContext) {
console.log('LongRunningRequestContext found, proceeding ' +
'with ping cycle logic: %j', event.LongRunningRequestContext)
if (resourceDefinition.LongRunning &&
resourceDefinition.LongRunning.MaxPings <=
event.LongRunningRequestContext.PassedPings) {
console.error('Ping cycle on long running resource ' +
'checks exceeded, timeout failure.')
return NormalReply('FATAL: LongRunning resource failed ' +
'to stabilize within MaxPings (' +
resourceDefinition.LongRunning.MaxPings + ' of ' +
resourceDefinition.LongRunning.PingInSeconds + ' seconds each)')
}
console.log('Inside LongRunning request ping cycle and not timed out, ' +
'diverting %s to handler with notDone callback supplied.', RequestType)
if (RequestType === 'Create') {
return resourceDefinition.LongRunning.Methods.Create(
event.LongRunningRequestContext,
Params,
NormalReply,
notDoneCallback)
} else if (RequestType === 'Update') {
return resourceDefinition.LongRunning.Methods.Update(
event.LongRunningRequestContext,
RequestPhysicalId,
Params,
OldParams,
NormalReply,
notDoneCallback)
} else {
return resourceDefinition.LongRunning.Methods.Delete(
event.LongRunningRequestContext,
RequestPhysicalId,
Params,
NormalReply,
notDoneCallback)
}
}
function notDoneCallback() {
console.log('Got NotDone signal callback from implementation of ' +
'cfn-lambda resource, engaging another tick in the cycle.')
triggerLongRunningReply(event.LongRunningRequestContext.RawResponse)
}
function triggerLongRunningReply(rawReplyResponse) {
if (rawReplyResponse.Status === 'FAILED') {
return sendResponse(rawReplyResponse)
}
console.log('Long running configurations found and ' +
'initialization sent SUCCESS, continuing with ' +
'recurse operation: %j', rawReplyResponse)
event.LongRunningRequestContext = {
RawResponse: rawReplyResponse,
PhysicalResourceId: rawReplyResponse.PhysicalResourceId,
Data: rawReplyResponse.Data,
PassedPings: event.LongRunningRequestContext
? event.LongRunningRequestContext.PassedPings + 1
: 0
}
console.log('In %s seconds, will recurse with event: %j',
resourceDefinition.LongRunning.PingInSeconds, event)
setTimeout(function() {
console.log('PingInSeconds of %s seconds passed, recursing lambda with: %j',
resourceDefinition.LongRunning.PingInSeconds, event)
resourceDefinition.LongRunning.LambdaApi.invoke({
FunctionName: CfnLambdaFactory.Environment.LambdaArn,
InvocationType: 'Event',
// Still CloudWatch logs, just not req/res here
LogType: 'None',
Payload: JSON.stringify(event)
}, function(invokeErr, invokeData) {
if (invokeErr) {
console.error('Was unable to trigger long running ' +
'pingback step: %j', invokeErr.message)
return NormalReply('Was unable to trigger long running ' +
'pingback step: ' + invokeErr.message)
}
console.log('Triggered long running ping step: %j', invokeData)
console.log('Terminating this lambda and allowing ' +
' lambda recursion to take over.')
context.done()
})
}, resourceDefinition.LongRunning.PingInSeconds * 1000)
}
var invalidation = ValidationCheck(Params, {
Validate: resourceDefinition.Validate,
Schema: resourceDefinition.Schema,
SchemaPath: resourceDefinition.SchemaPath
})
if (invalidation) {
if (RequestType === 'Delete') {
console.log('cfn-lambda: Got Delete with an invalidation, ' +
'tripping failsafe for ROLLBACK states and exiting with success.')
return NormalReply()
}
console.log('cfn-lambda: Found an invalidation.')
return NormalReply(invalidation)
}
if (RequestType === 'Create') {
console.log('cfn-lambda: Delegating to Create handler.')
return resourceDefinition.Create(Params, replyOrLongRunning('Create'))
}
if (RequestType === 'Update') {
if (JSONDeepEquals(Params, OldParams)) {
console.log('cfn-lambda: Delegating to NoUpdate handler, ' +
'or exiting with success (Update with unchanged params).')
return 'function' === typeof resourceDefinition.NoUpdate
? resourceDefinition.NoUpdate(RequestPhysicalId, Params, NormalReply)
: NormalReply(null, RequestPhysicalId)
}
if (Array.isArray(resourceDefinition.TriggersReplacement) &&
!PluckedEquality(resourceDefinition.TriggersReplacement, Params, OldParams)) {
console.log('cfn-lambda: Caught Replacement trigger key change, ' +
'delegating to Create, Delete will be called on old resource ' +
'during UPDATE_COMPLETE_CLEANUP_IN_PROGRESS phase.')
return resourceDefinition.Create(Params, replyOrLongRunning('Create'))
}
console.log('cfn-lambda: Delegating to Update handler.')
return resourceDefinition.Update(RequestPhysicalId,
Params, OldParams, replyOrLongRunning('Update'))
}
if (RequestType === 'Delete') {
console.log('cfn-lambda: Delegating to Delete handler.')
return resourceDefinition.Delete(RequestPhysicalId, Params, replyOrLongRunning('Delete'))
}
console.log('cfn-lambda: Uh oh! Called with unrecognized EventType!')
return NormalReply('The impossible happend! ' +
'CloudFormation sent an unknown RequestType.')
function replyWithFunctor(functor) {
return function(err, physicalId, optionalData) {
if (err) {
return functor({
Status: 'FAILED',
Reason: err.toString(),
PhysicalResourceId: physicalId ||
RequestPhysicalId ||
[event.StackId, event.LogicalResourceId, event.RequestId].join('/'),
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: optionalData
})
}
return functor({
Status: 'SUCCESS',
PhysicalResourceId: physicalId ||
RequestPhysicalId ||
[event.StackId, event.LogicalResourceId, event.RequestId].join('/'),
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: optionalData || OldParams
})
}
}
function sendResponse(response) {
var responseBody = JSON.stringify(response)
console.log('RESPONSE: %j', response)
console.log('REPLYING TO: %s', event.ResponseURL)
var parsedUrl = Url.parse(event.ResponseURL)
var options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || 443,
path: parsedUrl.path,
rejectUnauthorized: parsedUrl.hostname !== 'localhost',
method: 'PUT',
headers: {
'Content-Type': '',
'Content-Length': responseBody.length
}
}
if (parsedUrl.hostname === 'localhost') {
options.rejectUnauthorized = false
}
var request = Https.request(options, function(response) {
console.log('STATUS: %s',response.statusCode)
console.log('HEADERS: %j', response.headers)
response.on('data', function() {
// noop
})
response.on('end', function() {
// Tell AWS Lambda that the function execution is done
context.done()
})
})
request.on('error', function(error) {
console.log('sendResponse Error:\n', error)
// Tell AWS Lambda that the function execution is done
context.done()
})
// write data to request body
request.write(responseBody)
request.end()
}
}
}
CfnLambdaFactory.SDKAlias = SDKAlias
CfnLambdaFactory.ValidationCheck = ValidationCheck
CfnLambdaFactory.JSONDeepEquals = JSONDeepEquals
CfnLambdaFactory.PluckedEquality = PluckedEquality
CfnLambdaFactory.DefaultExpander = DefaultExpander
CfnLambdaFactory.Composite = Composite
CfnLambdaFactory.Module = Composite.Module
module.exports = CfnLambdaFactory
module.exports.deploy = require('./deploy')