1 | 'use strict'
|
2 |
|
3 | const path = require('path')
|
4 | const os = require('os')
|
5 | const aws = require('aws-sdk')
|
6 | const {exec, execSync, execFile} = require('child_process')
|
7 | const fs = require('fs-extra')
|
8 | const klaw = require('klaw')
|
9 | const packageJson = require(path.join(__dirname, '..', 'package.json'))
|
10 | const minimatch = require('minimatch')
|
11 | const archiver = require('archiver')
|
12 | const dotenv = require('dotenv')
|
13 | const proxy = require('proxy-agent')
|
14 | const ScheduleEvents = require(path.join(__dirname, 'schedule_events'))
|
15 | const S3Events = require(path.join(__dirname, 's3_events'))
|
16 | const S3Deploy = require(path.join(__dirname, 's3_deploy'))
|
17 | const CloudWatchLogs = require(path.join(__dirname, 'cloudwatch_logs'))
|
18 |
|
19 | const AWSXRay = require('aws-xray-sdk-core')
|
20 | const {createNamespace} = require('continuation-local-storage')
|
21 |
|
22 | const maxBufferSize = 50 * 1024 * 1024
|
23 |
|
24 | class Lambda {
|
25 | constructor () {
|
26 | this.version = packageJson.version
|
27 | }
|
28 |
|
29 | _createSampleFile (file, boilerplateName) {
|
30 | const exampleFile = path.join(process.cwd(), file)
|
31 | const boilerplateFile = path.join(
|
32 | __dirname,
|
33 | (boilerplateName || file) + '.example'
|
34 | )
|
35 |
|
36 | if (!fs.existsSync(exampleFile)) {
|
37 | fs.writeFileSync(exampleFile, fs.readFileSync(boilerplateFile))
|
38 | console.log(exampleFile + ' file successfully created')
|
39 | }
|
40 | }
|
41 |
|
42 | setup (program) {
|
43 | console.log('Running setup.')
|
44 | this._createSampleFile('.env', '.env')
|
45 | this._createSampleFile(program.eventFile, 'event.json')
|
46 | this._createSampleFile('deploy.env', 'deploy.env')
|
47 | this._createSampleFile(program.contextFile, 'context.json')
|
48 | this._createSampleFile('event_sources.json', 'event_sources.json')
|
49 | console.log(`Setup done.
|
50 | Edit the .env, deploy.env, ${program.contextFile}, \
|
51 | event_sources.json and ${program.eventFile} files as needed.`)
|
52 | }
|
53 |
|
54 | run (program) {
|
55 | if (!['nodejs4.3', 'nodejs6.10', 'nodejs8.10'].includes(program.runtime)) {
|
56 | console.error(`Runtime [${program.runtime}] is not supported.`)
|
57 | process.exit(254)
|
58 | }
|
59 |
|
60 | this._createSampleFile(program.eventFile, 'event.json')
|
61 | const splitHandler = program.handler.split('.')
|
62 | const filename = splitHandler[0] + '.js'
|
63 | const handlername = splitHandler[1]
|
64 |
|
65 |
|
66 | if (program.configFile) {
|
67 | this._setRunTimeEnvironmentVars(program)
|
68 | }
|
69 |
|
70 | const handler = require(path.join(process.cwd(), filename))[handlername]
|
71 | const event = require(path.join(process.cwd(), program.eventFile))
|
72 | const context = require(path.join(process.cwd(), program.contextFile))
|
73 | const enableRunMultipleEvents = (() => {
|
74 | if (typeof program.enableRunMultipleEvents === 'boolean') {
|
75 | return program.enableRunMultipleEvents
|
76 | }
|
77 | return program.enableRunMultipleEvents === 'true'
|
78 | })()
|
79 |
|
80 | if (Array.isArray(event) && enableRunMultipleEvents === true) {
|
81 | return this._runMultipleHandlers(event)
|
82 | }
|
83 | context.local = true
|
84 | this._runHandler(handler, event, program, context)
|
85 | }
|
86 |
|
87 | _runHandler (handler, event, program, context) {
|
88 | const startTime = new Date()
|
89 | const timeout = Math.min(program.timeout, 300) * 1000
|
90 |
|
91 | const callback = (err, result) => {
|
92 | if (err) {
|
93 | process.exitCode = 255
|
94 | console.log('Error: ' + err)
|
95 | } else {
|
96 | process.exitCode = 0
|
97 | console.log('Success:')
|
98 | if (result) {
|
99 | console.log(JSON.stringify(result))
|
100 | }
|
101 | }
|
102 | if (context.callbackWaitsForEmptyEventLoop === false) {
|
103 | process.exit()
|
104 | }
|
105 | }
|
106 |
|
107 | context.getRemainingTimeInMillis = () => {
|
108 | const currentTime = new Date()
|
109 | return timeout - (currentTime - startTime)
|
110 | }
|
111 |
|
112 |
|
113 |
|
114 |
|
115 | context.succeed = (result) => console.log(JSON.stringify(result))
|
116 | context.fail = (error) => console.log(JSON.stringify(error))
|
117 | context.done = (error, results) => {
|
118 | console.log(JSON.stringify(error))
|
119 | console.log(JSON.stringify(results))
|
120 | }
|
121 |
|
122 | const nameSpace = createNamespace('AWSXRay')
|
123 | nameSpace.run(() => {
|
124 | nameSpace.set('segment', new AWSXRay.Segment('annotations'))
|
125 | const result = handler(event, context, callback)
|
126 | if (result != null) {
|
127 | Promise.resolve(result).then(
|
128 | resolved => {
|
129 | console.log('Result:')
|
130 | console.log(JSON.stringify(resolved))
|
131 | },
|
132 | rejected => {
|
133 | console.log('Error:')
|
134 | console.log(rejected)
|
135 | }
|
136 | )
|
137 | }
|
138 | })
|
139 | }
|
140 |
|
141 | _runMultipleHandlers (events) {
|
142 | console.log(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
143 | Usually you will receive a single Object from AWS Lambda.
|
144 | We added support for event.json to contain an array,
|
145 | so you can easily test run multiple events.
|
146 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
147 | `)
|
148 |
|
149 | const _argv = process.argv
|
150 | const eventFileOptionIndex = (() => {
|
151 | const index = _argv.indexOf('-j')
|
152 | if (index >= 0) return index
|
153 | return _argv.indexOf('--eventFile')
|
154 | })()
|
155 | _argv[0] = 'node'
|
156 |
|
157 |
|
158 |
|
159 | events.forEach((event, i) => {
|
160 | const tmpEventFile = `.${i}_tmp_event.json`
|
161 | const command = () => {
|
162 | if (eventFileOptionIndex === -1) {
|
163 | return _argv.concat(['-j', tmpEventFile]).join(' ')
|
164 | }
|
165 | _argv[eventFileOptionIndex + 1] = tmpEventFile
|
166 | return _argv.join(' ')
|
167 | }
|
168 |
|
169 | fs.writeFileSync(tmpEventFile, JSON.stringify(event))
|
170 | const stdout = execSync(command(), {
|
171 | maxBuffer: maxBufferSize,
|
172 | env: process.env
|
173 | })
|
174 | console.log('>>> Event:', event, '<<<')
|
175 | console.log(stdout.toString())
|
176 | fs.unlinkSync(tmpEventFile)
|
177 | })
|
178 | }
|
179 |
|
180 | _isUseS3 (program) {
|
181 | if (typeof program.deployUseS3 === 'boolean') {
|
182 | return program.deployUseS3
|
183 | }
|
184 | return program.deployUseS3 === 'true'
|
185 | }
|
186 |
|
187 | _params (program, buffer) {
|
188 | const params = {
|
189 | FunctionName: program.functionName +
|
190 | (program.environment ? '-' + program.environment : '') +
|
191 | (program.lambdaVersion ? '-' + program.lambdaVersion : ''),
|
192 | Code: {},
|
193 | Handler: program.handler,
|
194 | Role: program.role,
|
195 | Runtime: program.runtime,
|
196 | Description: program.description,
|
197 | MemorySize: program.memorySize,
|
198 | Timeout: program.timeout,
|
199 | Publish: (() => {
|
200 | if (typeof program.publish === 'boolean') {
|
201 | return program.publish
|
202 | }
|
203 | return program.publish === 'true'
|
204 | })(),
|
205 | VpcConfig: {
|
206 | SubnetIds: [],
|
207 | SecurityGroupIds: []
|
208 | },
|
209 | Environment: {
|
210 | Variables: null
|
211 | },
|
212 | KMSKeyArn: program.kmsKeyArn,
|
213 | DeadLetterConfig: {
|
214 | TargetArn: null
|
215 | },
|
216 | TracingConfig: {
|
217 | Mode: null
|
218 | }
|
219 | }
|
220 |
|
221 | if (this._isUseS3(program)) {
|
222 | params.Code = {
|
223 | S3Bucket: null,
|
224 | S3Key: null
|
225 | }
|
226 | } else {
|
227 | params.Code = { ZipFile: buffer }
|
228 | }
|
229 |
|
230 |
|
231 | params.FunctionName = params.FunctionName.replace(/[^a-zA-Z0-9-_]/g, '_')
|
232 |
|
233 | if (program.vpcSubnets && program.vpcSecurityGroups) {
|
234 | params.VpcConfig = {
|
235 | 'SubnetIds': program.vpcSubnets.split(','),
|
236 | 'SecurityGroupIds': program.vpcSecurityGroups.split(',')
|
237 | }
|
238 | }
|
239 | if (program.configFile) {
|
240 | const configValues = fs.readFileSync(program.configFile)
|
241 | const config = dotenv.parse(configValues)
|
242 |
|
243 | params.Environment = {
|
244 | Variables: config
|
245 | }
|
246 | }
|
247 | if (program.deadLetterConfigTargetArn !== undefined) {
|
248 | params.DeadLetterConfig = {
|
249 | TargetArn: program.deadLetterConfigTargetArn
|
250 | }
|
251 | }
|
252 | if (program.tracingConfig) {
|
253 | params.TracingConfig.Mode = program.tracingConfig
|
254 | }
|
255 |
|
256 | return params
|
257 | }
|
258 |
|
259 | _eventSourceList (program) {
|
260 | if (!program.eventSourceFile) {
|
261 | return {
|
262 | EventSourceMappings: null,
|
263 | ScheduleEvents: null,
|
264 | S3Events: null
|
265 | }
|
266 | }
|
267 | const list = fs.readJsonSync(program.eventSourceFile)
|
268 |
|
269 | if (Array.isArray(list)) {
|
270 |
|
271 | return {
|
272 | EventSourceMappings: list,
|
273 | ScheduleEvents: [],
|
274 | S3Events: []
|
275 | }
|
276 | }
|
277 | if (!list.EventSourceMappings) {
|
278 | list.EventSourceMappings = []
|
279 | }
|
280 | if (!list.ScheduleEvents) {
|
281 | list.ScheduleEvents = []
|
282 | }
|
283 | if (!list.S3Events) {
|
284 | list.S3Events = []
|
285 | }
|
286 | return list
|
287 | }
|
288 |
|
289 | _fileCopy (program, src, dest, excludeNodeModules) {
|
290 | const excludes = (() => {
|
291 | return [
|
292 | '.git*',
|
293 | '*.swp',
|
294 | '.editorconfig',
|
295 | '.lambda',
|
296 | 'deploy.env',
|
297 | '*.log',
|
298 | path.join('build', path.sep)
|
299 | ]
|
300 | .concat(program.excludeGlobs ? program.excludeGlobs.split(' ') : [])
|
301 | .concat(excludeNodeModules ? [path.join('node_modules')] : [])
|
302 | })()
|
303 |
|
304 |
|
305 | const dirBlobs = []
|
306 | const pattern = '{' + excludes.map((str) => {
|
307 | if (str.charAt(str.length - 1) === path.sep) {
|
308 | str = str.substr(0, str.length - 1)
|
309 | dirBlobs.push(str)
|
310 | }
|
311 | return str
|
312 | }).join(',') + '}'
|
313 | const dirPatternRegExp = new RegExp(`(${dirBlobs.join('|')})$`)
|
314 |
|
315 | return new Promise((resolve, reject) => {
|
316 | fs.mkdirs(dest, (err) => {
|
317 | if (err) return reject(err)
|
318 | const options = {
|
319 | dereference: true,
|
320 | filter: (src, dest) => {
|
321 | if (!program.prebuiltDirectory && src === 'package.json') {
|
322 |
|
323 | return true
|
324 | }
|
325 |
|
326 | if (!minimatch(src, pattern, { matchBase: true })) {
|
327 | return true
|
328 | }
|
329 |
|
330 | if (!dirPatternRegExp.test(src)) {
|
331 | return false
|
332 | }
|
333 | return !fs.statSync(src).isDirectory()
|
334 | }
|
335 | }
|
336 | fs.copy(src, dest, options, (err) => {
|
337 | if (err) return reject(err)
|
338 | resolve()
|
339 | })
|
340 | })
|
341 | })
|
342 | }
|
343 |
|
344 | _npmInstall (program, codeDirectory) {
|
345 | const dockerBaseOptions = [
|
346 | 'run', '--rm', '-v', `${codeDirectory}:/var/task`, '-w', '/var/task',
|
347 | program.dockerImage,
|
348 | 'npm', '-s', 'install', '--production'
|
349 | ]
|
350 | const npmInstallBaseOptions = [
|
351 | '-s',
|
352 | 'install',
|
353 | '--production',
|
354 | '--prefix', codeDirectory
|
355 | ]
|
356 |
|
357 | const params = (() => {
|
358 |
|
359 |
|
360 |
|
361 | if (program.dockerImage) {
|
362 | if (process.platform === 'win32') {
|
363 | return {
|
364 | command: 'cmd.exe',
|
365 | options: ['/c', 'docker'].concat(dockerBaseOptions)
|
366 | }
|
367 | }
|
368 | return {
|
369 | command: 'docker',
|
370 | options: dockerBaseOptions
|
371 | }
|
372 | }
|
373 |
|
374 |
|
375 | if (process.platform === 'win32') {
|
376 | return {
|
377 | command: 'cmd.exe',
|
378 | options: ['/c', 'npm']
|
379 | .concat(npmInstallBaseOptions)
|
380 | .concat(['--cwd', codeDirectory])
|
381 | }
|
382 | }
|
383 | return {
|
384 | command: 'npm',
|
385 | options: npmInstallBaseOptions
|
386 | }
|
387 | })()
|
388 |
|
389 | return new Promise((resolve, reject) => {
|
390 | execFile(params.command, params.options, {
|
391 | maxBuffer: maxBufferSize,
|
392 | env: process.env
|
393 | }, (err) => {
|
394 | if (err) return reject(err)
|
395 | resolve()
|
396 | })
|
397 | })
|
398 | }
|
399 |
|
400 | _postInstallScript (program, codeDirectory) {
|
401 | const scriptFilename = 'post_install.sh'
|
402 | const filePath = path.join(codeDirectory, scriptFilename)
|
403 | if (!fs.existsSync(filePath)) return Promise.resolve()
|
404 |
|
405 | const cmd = path.join(codeDirectory, scriptFilename) + ' ' + program.environment
|
406 | console.log('=> Running post install script ' + scriptFilename)
|
407 |
|
408 | return new Promise((resolve, reject) => {
|
409 | exec(cmd, {
|
410 | env: process.env,
|
411 | cwd: codeDirectory,
|
412 | maxBuffer: maxBufferSize
|
413 | }, (error, stdout, stderr) => {
|
414 | if (error) {
|
415 | return reject(new Error(`${error} stdout: ${stdout} stderr: ${stderr}`))
|
416 | }
|
417 | console.log('\t\t' + stdout)
|
418 | resolve()
|
419 | })
|
420 | })
|
421 | }
|
422 |
|
423 | _zip (program, codeDirectory) {
|
424 | console.log('=> Zipping repo. This might take up to 30 seconds')
|
425 |
|
426 | const tmpZipFile = path.join(os.tmpdir(), +(new Date()) + '.zip')
|
427 | const output = fs.createWriteStream(tmpZipFile)
|
428 | const archive = archiver('zip', {
|
429 | zlib: { level: 9 }
|
430 | })
|
431 | return new Promise((resolve) => {
|
432 | output.on('close', () => {
|
433 | const contents = fs.readFileSync(tmpZipFile)
|
434 | fs.unlinkSync(tmpZipFile)
|
435 | resolve(contents)
|
436 | })
|
437 | archive.pipe(output)
|
438 | klaw(codeDirectory)
|
439 | .on('data', (file) => {
|
440 | if (file.stats.isDirectory()) return
|
441 |
|
442 | const filePath = file.path.replace(path.join(codeDirectory, path.sep), '')
|
443 | if (file.stats.isSymbolicLink()) {
|
444 | return archive.symlink(filePath, fs.readlinkSync(file.path))
|
445 | }
|
446 |
|
447 | archive.append(
|
448 | fs.createReadStream(file.path),
|
449 | {
|
450 | name: filePath,
|
451 | stats: file.stats
|
452 | }
|
453 | )
|
454 | })
|
455 | .on('end', () => {
|
456 | archive.finalize()
|
457 | })
|
458 | })
|
459 | }
|
460 |
|
461 | _codeDirectory () {
|
462 | return path.join(os.tmpdir(), `${path.basename(path.resolve('.'))}-lambda`)
|
463 | }
|
464 |
|
465 | _cleanDirectory (codeDirectory) {
|
466 | return new Promise((resolve, reject) => {
|
467 | fs.remove(codeDirectory, (err) => {
|
468 | if (err) return reject(err)
|
469 | resolve()
|
470 | })
|
471 | }).then(() => {
|
472 | return new Promise((resolve, reject) => {
|
473 | fs.mkdirs(codeDirectory, (err) => {
|
474 | if (err) return reject(err)
|
475 | resolve()
|
476 | })
|
477 | })
|
478 | })
|
479 | }
|
480 |
|
481 | _setRunTimeEnvironmentVars (program) {
|
482 | const configValues = fs.readFileSync(program.configFile)
|
483 | const config = dotenv.parse(configValues)
|
484 |
|
485 | for (let k in config) {
|
486 | if (!config.hasOwnProperty(k)) {
|
487 | continue
|
488 | }
|
489 |
|
490 | process.env[k] = config[k]
|
491 | }
|
492 | }
|
493 |
|
494 | _uploadExisting (lambda, params) {
|
495 | const _params = Object.assign({
|
496 | 'FunctionName': params.FunctionName,
|
497 | 'Publish': params.Publish
|
498 | }, params.Code)
|
499 | return new Promise((resolve, reject) => {
|
500 | const request = lambda.updateFunctionCode(_params, (err) => {
|
501 | if (err) return reject(err)
|
502 |
|
503 | lambda.updateFunctionConfiguration({
|
504 | 'FunctionName': params.FunctionName,
|
505 | 'Description': params.Description,
|
506 | 'Handler': params.Handler,
|
507 | 'MemorySize': params.MemorySize,
|
508 | 'Role': params.Role,
|
509 | 'Timeout': params.Timeout,
|
510 | 'Runtime': params.Runtime,
|
511 | 'VpcConfig': params.VpcConfig,
|
512 | 'Environment': params.Environment,
|
513 | 'KMSKeyArn': params.KMSKeyArn,
|
514 | 'DeadLetterConfig': params.DeadLetterConfig,
|
515 | 'TracingConfig': params.TracingConfig
|
516 | }, (err, data) => {
|
517 | if (err) return reject(err)
|
518 | resolve(data)
|
519 | })
|
520 | })
|
521 |
|
522 | request.on('retry', (response) => {
|
523 | console.log(response.error.message)
|
524 | console.log('=> Retrying')
|
525 | })
|
526 | })
|
527 | }
|
528 |
|
529 | _uploadNew (lambda, params) {
|
530 | return new Promise((resolve, reject) => {
|
531 | const request = lambda.createFunction(params, (err, data) => {
|
532 | if (err) return reject(err)
|
533 | resolve(data)
|
534 | })
|
535 | request.on('retry', (response) => {
|
536 | console.log(response.error.message)
|
537 | console.log('=> Retrying')
|
538 | })
|
539 | })
|
540 | }
|
541 |
|
542 | _readArchive (program) {
|
543 | if (!fs.existsSync(program.deployZipfile)) {
|
544 | const err = new Error('No such Zipfile [' + program.deployZipfile + ']')
|
545 | return Promise.reject(err)
|
546 | }
|
547 | return new Promise((resolve, reject) => {
|
548 | fs.readFile(program.deployZipfile, (err, data) => {
|
549 | if (err) return reject(err)
|
550 | resolve(data)
|
551 | })
|
552 | })
|
553 | }
|
554 |
|
555 | _archive (program) {
|
556 | if (program.deployZipfile && fs.existsSync(program.deployZipfile)) {
|
557 | return this._readArchive(program)
|
558 | }
|
559 | return program.prebuiltDirectory
|
560 | ? this._archivePrebuilt(program)
|
561 | : this._buildAndArchive(program)
|
562 | }
|
563 |
|
564 | _archivePrebuilt (program) {
|
565 | const codeDirectory = this._codeDirectory()
|
566 |
|
567 | return this._fileCopy(program, program.prebuiltDirectory, codeDirectory, false).then(() => {
|
568 | console.log('=> Zipping deployment package')
|
569 | return this._zip(program, codeDirectory)
|
570 | })
|
571 | }
|
572 |
|
573 | _buildAndArchive (program) {
|
574 | if (!fs.existsSync('.env')) {
|
575 | console.warn('[Warning] `.env` file does not exist.')
|
576 | console.info('Execute `node-lambda setup` as necessary and set it up.')
|
577 | }
|
578 |
|
579 |
|
580 | const arch = process.platform + '.' + process.arch
|
581 | if (arch !== 'linux.x64' && !program.dockerImage) {
|
582 | console.warn(`Warning!!! You are building on a platform that is not 64-bit Linux (${arch}).
|
583 | If any of your Node dependencies include C-extensions, \
|
584 | they may not work as expected in the Lambda environment.
|
585 |
|
586 | `)
|
587 | }
|
588 |
|
589 | const codeDirectory = this._codeDirectory()
|
590 | const lambdaSrcDirectory = program.sourceDirectory ? program.sourceDirectory.replace(/\/$/, '') : '.'
|
591 |
|
592 | return Promise.resolve().then(() => {
|
593 | return this._cleanDirectory(codeDirectory)
|
594 | }).then(() => {
|
595 | console.log('=> Moving files to temporary directory')
|
596 | return this._fileCopy(program, lambdaSrcDirectory, codeDirectory, true)
|
597 | }).then(() => {
|
598 | console.log('=> Running npm install --production')
|
599 | return this._npmInstall(program, codeDirectory)
|
600 | }).then(() => {
|
601 | return this._postInstallScript(program, codeDirectory)
|
602 | }).then(() => {
|
603 | console.log('=> Zipping deployment package')
|
604 | return this._zip(program, codeDirectory)
|
605 | })
|
606 | }
|
607 |
|
608 | _listEventSourceMappings (lambda, params) {
|
609 | return new Promise((resolve, reject) => {
|
610 | lambda.listEventSourceMappings(params, (err, data) => {
|
611 | if (err) return reject(err)
|
612 | if (data && data.EventSourceMappings) {
|
613 | return resolve(data.EventSourceMappings)
|
614 | }
|
615 | return resolve([])
|
616 | })
|
617 | })
|
618 | }
|
619 |
|
620 | _updateEventSources (lambda, functionName, existingEventSourceList, eventSourceList) {
|
621 | if (eventSourceList == null) {
|
622 | return Promise.resolve([])
|
623 | }
|
624 | const updateEventSourceList = []
|
625 |
|
626 | for (let i in eventSourceList) {
|
627 | let isExisting = false
|
628 | for (let j in existingEventSourceList) {
|
629 | if (eventSourceList[i]['EventSourceArn'] === existingEventSourceList[j]['EventSourceArn']) {
|
630 | isExisting = true
|
631 | updateEventSourceList.push({
|
632 | 'type': 'update',
|
633 | 'FunctionName': functionName,
|
634 | 'Enabled': eventSourceList[i]['Enabled'],
|
635 | 'BatchSize': eventSourceList[i]['BatchSize'],
|
636 | 'UUID': existingEventSourceList[j]['UUID']
|
637 | })
|
638 | break
|
639 | }
|
640 | }
|
641 |
|
642 |
|
643 | if (!isExisting) {
|
644 | updateEventSourceList.push({
|
645 | 'type': 'create',
|
646 | 'FunctionName': functionName,
|
647 | 'EventSourceArn': eventSourceList[i]['EventSourceArn'],
|
648 | 'Enabled': eventSourceList[i]['Enabled'] ? eventSourceList[i]['Enabled'] : false,
|
649 | 'BatchSize': eventSourceList[i]['BatchSize'] ? eventSourceList[i]['BatchSize'] : 100,
|
650 | 'StartingPosition': eventSourceList[i]['StartingPosition'] ? eventSourceList[i]['StartingPosition'] : 'LATEST'
|
651 | })
|
652 | }
|
653 | }
|
654 |
|
655 |
|
656 | for (let i in existingEventSourceList) {
|
657 | let isExisting = false
|
658 | for (let j in eventSourceList) {
|
659 | if (eventSourceList[j]['EventSourceArn'] === existingEventSourceList[i]['EventSourceArn']) {
|
660 | isExisting = true
|
661 | break
|
662 | }
|
663 | }
|
664 |
|
665 |
|
666 | if (!isExisting) {
|
667 | updateEventSourceList.push({
|
668 | 'type': 'delete',
|
669 | 'UUID': existingEventSourceList[i]['UUID']
|
670 | })
|
671 | }
|
672 | }
|
673 |
|
674 | return Promise.all(updateEventSourceList.map((updateEventSource) => {
|
675 | switch (updateEventSource['type']) {
|
676 | case 'create':
|
677 | delete updateEventSource['type']
|
678 | return new Promise((resolve, reject) => {
|
679 | lambda.createEventSourceMapping(updateEventSource, (err, data) => {
|
680 | if (err) return reject(err)
|
681 | resolve(data)
|
682 | })
|
683 | })
|
684 | case 'update':
|
685 | delete updateEventSource['type']
|
686 | return new Promise((resolve, reject) => {
|
687 | lambda.updateEventSourceMapping(updateEventSource, (err, data) => {
|
688 | if (err) return reject(err)
|
689 | resolve(data)
|
690 | })
|
691 | })
|
692 | case 'delete':
|
693 | delete updateEventSource['type']
|
694 | return new Promise((resolve, reject) => {
|
695 | lambda.deleteEventSourceMapping(updateEventSource, (err, data) => {
|
696 | if (err) return reject(err)
|
697 | resolve(data)
|
698 | })
|
699 | })
|
700 | }
|
701 | return Promise.resolve()
|
702 | }))
|
703 | }
|
704 |
|
705 | _updateScheduleEvents (scheduleEvents, functionArn, scheduleList) {
|
706 | if (scheduleList == null) {
|
707 | return Promise.resolve([])
|
708 | }
|
709 |
|
710 | const paramsList = scheduleList.map((schedule) =>
|
711 | Object.assign(schedule, { FunctionArn: functionArn }))
|
712 |
|
713 |
|
714 | return paramsList.map((params) => {
|
715 | return scheduleEvents.add(params)
|
716 | }).reduce((a, b) => {
|
717 | return a.then(b)
|
718 | }, Promise.resolve()).then(() => {
|
719 |
|
720 |
|
721 |
|
722 | return paramsList
|
723 | })
|
724 | }
|
725 |
|
726 | _updateS3Events (s3Events, functionArn, s3EventsList) {
|
727 | if (s3EventsList == null) return Promise.resolve([])
|
728 |
|
729 | const paramsList = s3EventsList.map(s3event =>
|
730 | Object.assign(s3event, { FunctionArn: functionArn }))
|
731 |
|
732 | return s3Events.add(paramsList).then(() => {
|
733 |
|
734 | return paramsList
|
735 | })
|
736 | }
|
737 |
|
738 | _setLogsRetentionPolicy (cloudWatchLogs, program, functionName) {
|
739 | const days = parseInt(program.retentionInDays)
|
740 | if (!Number.isInteger(days)) return Promise.resolve({})
|
741 | return cloudWatchLogs.setLogsRetentionPolicy({
|
742 | FunctionName: functionName,
|
743 | retentionInDays: days
|
744 | }).then(() => {
|
745 |
|
746 | return { retentionInDays: days }
|
747 | })
|
748 | }
|
749 |
|
750 | package (program) {
|
751 | if (!program.packageDirectory) {
|
752 | throw new Error('packageDirectory not specified!')
|
753 | }
|
754 | try {
|
755 | const isDir = fs.lstatSync(program.packageDirectory).isDirectory()
|
756 |
|
757 | if (!isDir) {
|
758 | throw new Error(program.packageDirectory + ' is not a directory!')
|
759 | }
|
760 | } catch (err) {
|
761 | if (err.code !== 'ENOENT') {
|
762 | throw err
|
763 | }
|
764 | console.log('=> Creating package directory')
|
765 | fs.mkdirsSync(program.packageDirectory)
|
766 | }
|
767 |
|
768 | return this._archive(program).then((buffer) => {
|
769 | const basename = program.functionName + (program.environment ? '-' + program.environment : '')
|
770 | const zipfile = path.join(program.packageDirectory, basename + '.zip')
|
771 | console.log('=> Writing packaged zip')
|
772 | fs.writeFile(zipfile, buffer, (err) => {
|
773 | if (err) {
|
774 | throw err
|
775 | }
|
776 | console.log('Packaged zip created: ' + zipfile)
|
777 | })
|
778 | }).catch((err) => {
|
779 | throw err
|
780 | })
|
781 | }
|
782 |
|
783 | _awsConfigUpdate (program, region) {
|
784 | const awsSecurity = { region: region }
|
785 |
|
786 | if (program.profile) {
|
787 | aws.config.credentials = new aws.SharedIniFileCredentials({
|
788 | profile: program.profile
|
789 | })
|
790 | } else {
|
791 | awsSecurity.accessKeyId = program.accessKey
|
792 | awsSecurity.secretAccessKey = program.secretKey
|
793 | }
|
794 |
|
795 | if (program.sessionToken) {
|
796 | awsSecurity.sessionToken = program.sessionToken
|
797 | }
|
798 |
|
799 | if (program.deployTimeout) {
|
800 | aws.config.httpOptions.timeout = parseInt(program.deployTimeout)
|
801 | }
|
802 |
|
803 | if (program.proxy) {
|
804 | aws.config.httpOptions.agent = proxy(program.proxy)
|
805 | }
|
806 |
|
807 | if (program.endpoint) {
|
808 | aws.config.endpoint = program.endpoint
|
809 | }
|
810 |
|
811 | aws.config.update(awsSecurity)
|
812 | }
|
813 |
|
814 | _isFunctionDoesNotExist (err) {
|
815 | return err.code === 'ResourceNotFoundException' &&
|
816 | !!err.message.match(/^Function not found:/)
|
817 | }
|
818 |
|
819 | _deployToRegion (program, params, region, buffer) {
|
820 | this._awsConfigUpdate(program, region)
|
821 |
|
822 | console.log('=> Reading event source file to memory')
|
823 | const eventSourceList = this._eventSourceList(program)
|
824 |
|
825 | return Promise.resolve().then(() => {
|
826 | if (this._isUseS3(program)) {
|
827 | const s3Deploy = new S3Deploy(aws, region)
|
828 | return s3Deploy.putPackage(params, region, buffer)
|
829 | }
|
830 | return null
|
831 | }).then((code) => {
|
832 | if (code != null) params.Code = code
|
833 | }).then(() => {
|
834 | if (!this._isUseS3(program)) {
|
835 | console.log(`=> Uploading zip file to AWS Lambda ${region} with parameters:`)
|
836 | } else {
|
837 | console.log(`=> Uploading AWS Lambda ${region} with parameters:`)
|
838 | }
|
839 | console.log(params)
|
840 |
|
841 | const lambda = new aws.Lambda({
|
842 | region: region,
|
843 | apiVersion: '2015-03-31'
|
844 | })
|
845 | const scheduleEvents = new ScheduleEvents(aws, region)
|
846 | const s3Events = new S3Events(aws, region)
|
847 | const cloudWatchLogs = new CloudWatchLogs(aws, region)
|
848 |
|
849 |
|
850 | return lambda.getFunction({
|
851 | 'FunctionName': params.FunctionName
|
852 | }).promise().then(() => {
|
853 |
|
854 | return this._listEventSourceMappings(lambda, {
|
855 | 'FunctionName': params.FunctionName
|
856 | }).then((existingEventSourceList) => {
|
857 | return Promise.all([
|
858 | this._uploadExisting(lambda, params).then((results) => {
|
859 | console.log('=> Done uploading. Results follow: ')
|
860 | console.log(results)
|
861 | return results
|
862 | }).then(results => {
|
863 | return Promise.all([
|
864 | this._updateScheduleEvents(
|
865 | scheduleEvents,
|
866 | results.FunctionArn,
|
867 | eventSourceList.ScheduleEvents
|
868 | ),
|
869 | this._updateS3Events(
|
870 | s3Events,
|
871 | results.FunctionArn,
|
872 | eventSourceList.S3Events
|
873 | )
|
874 | ])
|
875 | }),
|
876 | this._updateEventSources(
|
877 | lambda,
|
878 | params.FunctionName,
|
879 | existingEventSourceList,
|
880 | eventSourceList.EventSourceMappings
|
881 | ),
|
882 | this._setLogsRetentionPolicy(
|
883 | cloudWatchLogs,
|
884 | program,
|
885 | params.FunctionName
|
886 | )
|
887 | ])
|
888 | })
|
889 | }).catch((err) => {
|
890 | if (!this._isFunctionDoesNotExist(err)) {
|
891 | throw err
|
892 | }
|
893 |
|
894 | return this._uploadNew(lambda, params).then((results) => {
|
895 | console.log('=> Done uploading. Results follow: ')
|
896 | console.log(results)
|
897 |
|
898 | return Promise.all([
|
899 | this._updateEventSources(
|
900 | lambda,
|
901 | params.FunctionName,
|
902 | [],
|
903 | eventSourceList.EventSourceMappings
|
904 | ),
|
905 | this._updateScheduleEvents(
|
906 | scheduleEvents,
|
907 | results.FunctionArn,
|
908 | eventSourceList.ScheduleEvents
|
909 | ),
|
910 | this._updateS3Events(
|
911 | s3Events,
|
912 | results.FunctionArn,
|
913 | eventSourceList.S3Events
|
914 | ),
|
915 | this._setLogsRetentionPolicy(
|
916 | cloudWatchLogs,
|
917 | program,
|
918 | params.FunctionName
|
919 | )
|
920 | ])
|
921 | })
|
922 | })
|
923 | })
|
924 | }
|
925 |
|
926 | _printDeployResults (results, isFirst) {
|
927 | if (!Array.isArray(results)) {
|
928 | if (results == null) return
|
929 | console.log(results)
|
930 | return
|
931 | }
|
932 | if (results.length === 0) return
|
933 |
|
934 | if (isFirst === true) console.log('=> All tasks done. Results follow:')
|
935 | results.forEach(result => {
|
936 | this._printDeployResults(result)
|
937 | })
|
938 | }
|
939 |
|
940 | deploy (program) {
|
941 | const regions = program.region.split(',')
|
942 | return this._archive(program).then((buffer) => {
|
943 | console.log('=> Reading zip file to memory')
|
944 | return buffer
|
945 | }).then((buffer) => {
|
946 | const params = this._params(program, buffer)
|
947 |
|
948 | return Promise.all(regions.map((region) => {
|
949 | return this._deployToRegion(
|
950 | program,
|
951 | params,
|
952 | region,
|
953 | this._isUseS3(program) ? buffer : null
|
954 | )
|
955 | })).then(results => {
|
956 | this._printDeployResults(results, true)
|
957 | })
|
958 | }).catch((err) => {
|
959 | process.exitCode = 1
|
960 | console.log(err)
|
961 | })
|
962 | }
|
963 | }
|
964 |
|
965 | module.exports = new Lambda()
|