From bdd01d7284311061d260fc99962e9768357c3606 Mon Sep 17 00:00:00 2001 From: Blair Garrett Date: Mon, 21 Nov 2016 22:08:03 +0000 Subject: [PATCH] Upgraded to ES6. Added AirBnb EsLint file. Added support for Flow. --- .gitignore | 3 +- index.js | 218 +++++++++++++++++++++++++--------------------- package.json | 19 +++- test/CHANGELOG.md | 40 +++++++++ test/README.md | 100 +++++++++++++++++++++ test/index.js | 188 +++++++++++++++++++++++++++++++++++++++ test/package.json | 43 +++++++++ 7 files changed, 507 insertions(+), 104 deletions(-) create mode 100644 test/CHANGELOG.md create mode 100644 test/README.md create mode 100644 test/index.js create mode 100644 test/package.json diff --git a/.gitignore b/.gitignore index 5a23aa6..adceed6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/node_modules/* +node_modules +test/node_modules diff --git a/index.js b/index.js index 773f76d..c37e0ae 100644 --- a/index.js +++ b/index.js @@ -1,16 +1,23 @@ -var fs = require('fs'); -var AWS = require('aws-sdk'); -var extend = require('util')._extend; -var async = require('async'); +// @flow + +import { stat } from 'fs'; +import { AWS } from 'aws-sdk'; +import { extend } from 'util-extend'; +import { async } from 'async'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { loggerpro } from 'loggerpro'; + +exports.deploy = (codePackage: string, config: any, callback: any, logger: any, lambda: any) => { + let loggerImpl; + let lambdaNew = lambda; -exports.deploy = function(codePackage, config, callback, logger, lambda) { if (!logger) { - logger = console.log; + loggerImpl = loggerpro; } - if(!lambda) { - if("profile" in config) { - var credentials = new AWS.SharedIniFileCredentials({profile: config.profile}); + if (!lambdaNew) { + if ('profile' in config) { + const credentials = new AWS.SharedIniFileCredentials({ profile: config.profile }); AWS.config.credentials = credentials; } @@ -18,146 +25,159 @@ exports.deploy = function(codePackage, config, callback, logger, lambda) { if (!AWS.config.httpOptions) { AWS.config.httpOptions = {}; } - var HttpsProxyAgent = require('https-proxy-agent'); AWS.config.httpOptions.agent = new HttpsProxyAgent(process.env.HTTPS_PROXY); } - lambda = new AWS.Lambda({ + lambdaNew = new AWS.Lambda({ region: config.region, - accessKeyId: "accessKeyId" in config ? config.accessKeyId : "", - secretAccessKey: "secretAccessKey" in config ? config.secretAccessKey : "", - sessionToken: "sessionToken" in config ? config.sessionToken : "" + accessKeyId: 'accessKeyId' in config ? config.accessKeyId : '', + secretAccessKey: 'secretAccessKey' in config ? config.secretAccessKey : '', + sessionToken: 'sessionToken' in config ? config.sessionToken : '', }); } - var params = { + const params = { FunctionName: config.functionName, Description: config.description, Handler: config.handler, Role: config.role, Timeout: config.timeout, - MemorySize: config.memorySize + MemorySize: config.memorySize, + VpcConfig: null, + BatchSize: null, + Code: {}, + Runtime: '', + Publish: false, }; - if (config.vpc) params.VpcConfig = config.vpc; - var isPublish = (config.publish === true); - var updateEventSource = function(eventSource, callback) { - var params = extend({ - FunctionName: config.functionName + if (config.vpc) { + params.VpcConfig = config.vpc; + } + + const isPublish = (config.publish === true); + + const updateEventSource = (eventSource, updateEventSourceCallback) => { + const eventSourceParams = extend({ + FunctionName: config.functionName, }, eventSource); - lambda.listEventSourceMappings({ - FunctionName: params.FunctionName, - EventSourceArn: params.EventSourceArn - }, function(err, data) { - if(err) { - logger("List event source mapping failed, please make sure you have permission"); - callback(err); + lambdaNew.listEventSourceMappings({ + FunctionName: eventSourceParams.FunctionName, + EventSourceArn: eventSourceParams.EventSourceArn, + }, (err, data) => { + if (err) { + loggerImpl('List event source mapping failed, please make sure you have permission'); + updateEventSourceCallback(err); + } + + if (data.EventSourceMappings.length === 0) { + lambdaNew.createEventSourceMapping(eventSourceParams, + (eventSourceError) => { + if (eventSourceError) { + loggerImpl('Failed to create event source mapping!'); + updateEventSourceCallback(eventSourceError); + } else { + updateEventSourceCallback(); + } + }); } else { - if (data.EventSourceMappings.length === 0) { - lambda.createEventSourceMapping(params, function(err, data) { - if(err) { - logger("Failed to create event source mapping!"); - callback(err); - } else { - callback(); - } - }); - } else { - async.eachSeries(data.EventSourceMappings, function(mapping, iteratorCallback) { - lambda.updateEventSourceMapping({ - UUID: mapping.UUID, - BatchSize: params.BatchSize - }, iteratorCallback); - }, function(err) { - if(err) { - logger("Update event source mapping failed"); - callback(err); - } else { - callback(); - } - }); - } + async.eachSeries(data.EventSourceMappings, (mapping, iteratorCallback) => { + lambdaNew.updateEventSourceMapping({ + UUID: mapping.UUID, + BatchSize: params.BatchSize, + }, iteratorCallback); + }, (eventSourceMappingsError) => { + if (eventSourceMappingsError) { + loggerImpl('Update event source mapping failed'); + updateEventSourceCallback(eventSourceMappingsError); + } else { + updateEventSourceCallback(); + } + }); } }); }; - var updateEventSources = function(callback) { - var eventSources; - - if(!config.eventSource) { - callback(); + const updateEventSources = (updateEventSourcesCallback) => { + if (!config.eventSource) { + updateEventSourcesCallback(); return; } - eventSources = Array.isArray(config.eventSource) ? config.eventSource : [ config.eventSource ]; - + const eventSources = Array.isArray(config.eventSource) + ? config.eventSource : [config.eventSource]; async.eachSeries( eventSources, updateEventSource, - function(err) { - callback(err); - } + (err) => { + callback(err); + }, ); }; - var updateFunction = function(callback) { - fs.readFile(codePackage, function(err, data) { - if(err) { - return callback('Error reading specified package "'+ codePackage + '"'); + const updateFunction = (updateFunctionCallback) => { + stat.readFile(codePackage, (err, data) => { + if (err) { + const returnMessage = `Error reading specified package, ${codePackage}`; + return callback(returnMessage); } - lambda.updateFunctionCode({FunctionName: params.FunctionName, ZipFile: data, Publish: isPublish}, function(err, data) { - if (err) { - var warning = 'Package upload failed. ' - warning += 'Check your iam:PassRole permissions.' - logger(warning); - callback(err) - } else { - lambda.updateFunctionConfiguration(params, function(err, data) { - if (err) { - var warning = 'Update function configuration failed. ' - logger(warning); - callback(err); - } else { - updateEventSources(callback); - } - }); - } - }); + lambdaNew.updateFunctionCode({ + FunctionName: params.FunctionName, ZipFile: data, Publish: isPublish }, + (updateFunctionCodeError) => { + if (updateFunctionCodeError) { + let warning = 'Package upload failed. '; + warning += 'Check your iam:PassRole permissions.'; + logger(warning); + updateFunctionCallback(updateFunctionCodeError); + } else { + lambdaNew.updateFunctionConfiguration(params, (updateFunctionConfigurationError) => { + if (updateFunctionConfigurationError) { + const warning = 'Update function configuration failed. '; + logger(warning); + updateFunctionCallback(updateFunctionConfigurationError); + } else { + updateEventSources(updateFunctionCallback); + } + }); + } + }); + + return true; }); }; - var createFunction = function(callback) { - fs.readFile(codePackage, function(err, data) { - if(err) { - return callback('Error reading specified package "'+ codePackage + '"'); + const createFunction = (createFunctionCallback) => { + stat.readFile(codePackage, (createFunctionError, createFunctionData) => { + if (createFunctionError) { + return callback(`Error reading specified package ${codePackage}`); } - params['Code'] = { ZipFile: data }; - params['Runtime'] = "runtime" in config ? config.runtime : "nodejs4.3"; - params['Publish'] = isPublish; - lambda.createFunction(params, function(err, data) { + params.Code = { ZipFile: createFunctionData }; + params.Runtime = 'runtime' in config ? config.runtime : 'nodejs4.3'; + params.Publish = isPublish; + lambdaNew.createFunction(params, (err) => { if (err) { - var warning = 'Create function failed. ' - warning += 'Check your iam:PassRole permissions.' + let warning = 'Create function failed. '; + warning += 'Check your iam:PassRole permissions.'; logger(warning); - callback(err) + createFunctionCallback(err); } else { updateEventSources(callback); } }); + + return true; }); }; - - lambda.getFunction({FunctionName: params.FunctionName}, function(err, data) { + lambdaNew.getFunction({ FunctionName: params.FunctionName }, (err) => { if (err) { if (err.statusCode === 404) { createFunction(callback); } else { - var warning = 'AWS API request failed. ' - warning += 'Check your AWS credentials and permissions.' + let warning = 'AWS API request failed. '; + warning += 'Check your AWS credentials and permissions.'; logger(warning); callback(err); } diff --git a/package.json b/package.json index 37757b1..6ee05ab 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,11 @@ { "name": "node-aws-lambda", - "version": "0.1.8", + "version": "0.1.9", "description": "A module help you automate AWS lambda function deployment.", "scripts": { - "test": "./node_modules/mocha/bin/mocha test/all.js" + "test": "./node_modules/mocha/bin/mocha test/all.js", + "lint": "./node_modules/.bin/eslint **.js", + "flow": "flow; test $? -eq 0 -o $? -eq 2" }, "repository": { "type": "git", @@ -22,11 +24,20 @@ "dependencies": { "async": "^1.0.0", "aws-sdk": "2.x.x", - "https-proxy-agent": "^1.0.0" + "babel-eslint": "^7.1.1", + "eslint-plugin-flowtype": "^2.25.0", + "fs": "0.0.1-security", + "https-proxy-agent": "^1.0.0", + "loggerpro": "^1.0.2", + "util-extend": "^1.0.3" }, "devDependencies": { - "mocha": "^2.2.1", "chai": "^2.1.1", + "eslint": "^3.10.2", + "eslint-config-airbnb-base": "^10.0.1", + "eslint-plugin-import": "^2.2.0", + "flow-bin": "^0.35.0", + "mocha": "^2.2.1", "node-uuid": "^1.4.3" } } diff --git a/test/CHANGELOG.md b/test/CHANGELOG.md new file mode 100644 index 0000000..9381d06 --- /dev/null +++ b/test/CHANGELOG.md @@ -0,0 +1,40 @@ +0.1.9 +===== +* Converted to ES6 (by @blairg) +* Added AirBnb EsLint configuration (by @blairg) +* Support for Flow (by @blairg) + +0.1.8 +===== +* Support VPC (by @tilfin) +* Support authentication with sessionToken (by @JohnBloom) +* Support Publish flag (by @hiro-koba) + +0.1.7 +===== +* Support python and other runtime (by @kikusu) +* Support specifying multiple event sources (by @driadi) + +0.1.6 +===== +* Support specifying descriptions (by @dlhdesign) + +0.1.5 +===== +* HTTPS proxy support (by @dvonlehman) + +0.1.4 +===== +* Add "profile" option to enable load AWS credentials from custom profile (by @bryannaegele) + +0.1.3 +===== +* Add optional accessKeyId and secretAccessKey (by @ikait) + +0.1.2 +===== +* Add support for memorySize option (by @ikait) + +0.1.1 +===== +* Skip event source mapping if not specified. diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..6078313 --- /dev/null +++ b/test/README.md @@ -0,0 +1,100 @@ +# node-aws-lambda [![npm version](https://badge.fury.io/js/node-aws-lambda.svg)](http://badge.fury.io/js/node-aws-lambda) [![Build Status](https://snap-ci.com/ThoughtWorksStudios/node-aws-lambda/branch/master/build_image)](https://snap-ci.com/ThoughtWorksStudios/node-aws-lambda/branch/master) +[Built with :yellow_heart: and :coffee: in San Francisco](http://www.thoughtworks.com/mingle/team/) + +A module helps you automate AWS lambda function deployment. +All lambda configuration is managed in the codebase, includes event source mappings. So you can version control everything and automate the deployment instead of click click click in AWS console. + +Inspired by https://medium.com/@AdamRNeary/a-gulp-workflow-for-amazon-lambda-61c2afd723b6 + +# Gulp example: + +gulpfile.js +```node +var gulp = require('gulp'); +var zip = require('gulp-zip'); +var del = require('del'); +var install = require('gulp-install'); +var runSequence = require('run-sequence'); +var awsLambda = require("node-aws-lambda"); + +gulp.task('clean', function() { + return del(['./dist', './dist.zip']); +}); + +gulp.task('js', function() { + return gulp.src('index.js') + .pipe(gulp.dest('dist/')); +}); + +gulp.task('node-mods', function() { + return gulp.src('./package.json') + .pipe(gulp.dest('dist/')) + .pipe(install({production: true})); +}); + +gulp.task('zip', function() { + return gulp.src(['dist/**/*', '!dist/package.json']) + .pipe(zip('dist.zip')) + .pipe(gulp.dest('./')); +}); + +gulp.task('upload', function(callback) { + awsLambda.deploy('./dist.zip', require("./lambda-config.js"), callback); +}); + +gulp.task('deploy', function(callback) { + return runSequence( + ['clean'], + ['js', 'node-mods'], + ['zip'], + ['upload'], + callback + ); +}); +``` +lambda-config.js + +```node +module.exports = { + accessKeyId: , // optional + secretAccessKey: , // optional + sessionToken: , // optional + profile: , // optional for loading AWS credientail from custom profile + region: 'us-east-1', + handler: 'index.handler', + role: , + functionName: , + timeout: 10, + memorySize: 128, + publish: true, // default: false, + runtime: 'nodejs4.3', // default: 'nodejs4.3', + vpc: { // optional + SecurityGroupIds: [, ...], + SubnetIds: [, ...] + }, + eventSource: { + EventSourceArn: , + BatchSize: 200, + StartingPosition: "TRIM_HORIZON" + } +} +```` + +# Proxy setup +Deployment via https proxy is supported by setting environment variable "HTTPS_PROXY". For example: + +```terminal +> HTTPS_PROXY="https://myproxy:8080" gulp deploy +``` + +# License + +(The MIT License) + +Copyright (c) 2015 ThoughtWorks Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..c37e0ae --- /dev/null +++ b/test/index.js @@ -0,0 +1,188 @@ +// @flow + +import { stat } from 'fs'; +import { AWS } from 'aws-sdk'; +import { extend } from 'util-extend'; +import { async } from 'async'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { loggerpro } from 'loggerpro'; + +exports.deploy = (codePackage: string, config: any, callback: any, logger: any, lambda: any) => { + let loggerImpl; + let lambdaNew = lambda; + + if (!logger) { + loggerImpl = loggerpro; + } + + if (!lambdaNew) { + if ('profile' in config) { + const credentials = new AWS.SharedIniFileCredentials({ profile: config.profile }); + AWS.config.credentials = credentials; + } + + if (process.env.HTTPS_PROXY) { + if (!AWS.config.httpOptions) { + AWS.config.httpOptions = {}; + } + AWS.config.httpOptions.agent = new HttpsProxyAgent(process.env.HTTPS_PROXY); + } + + lambdaNew = new AWS.Lambda({ + region: config.region, + accessKeyId: 'accessKeyId' in config ? config.accessKeyId : '', + secretAccessKey: 'secretAccessKey' in config ? config.secretAccessKey : '', + sessionToken: 'sessionToken' in config ? config.sessionToken : '', + }); + } + + const params = { + FunctionName: config.functionName, + Description: config.description, + Handler: config.handler, + Role: config.role, + Timeout: config.timeout, + MemorySize: config.memorySize, + VpcConfig: null, + BatchSize: null, + Code: {}, + Runtime: '', + Publish: false, + }; + + if (config.vpc) { + params.VpcConfig = config.vpc; + } + + const isPublish = (config.publish === true); + + const updateEventSource = (eventSource, updateEventSourceCallback) => { + const eventSourceParams = extend({ + FunctionName: config.functionName, + }, eventSource); + + lambdaNew.listEventSourceMappings({ + FunctionName: eventSourceParams.FunctionName, + EventSourceArn: eventSourceParams.EventSourceArn, + }, (err, data) => { + if (err) { + loggerImpl('List event source mapping failed, please make sure you have permission'); + updateEventSourceCallback(err); + } + + if (data.EventSourceMappings.length === 0) { + lambdaNew.createEventSourceMapping(eventSourceParams, + (eventSourceError) => { + if (eventSourceError) { + loggerImpl('Failed to create event source mapping!'); + updateEventSourceCallback(eventSourceError); + } else { + updateEventSourceCallback(); + } + }); + } else { + async.eachSeries(data.EventSourceMappings, (mapping, iteratorCallback) => { + lambdaNew.updateEventSourceMapping({ + UUID: mapping.UUID, + BatchSize: params.BatchSize, + }, iteratorCallback); + }, (eventSourceMappingsError) => { + if (eventSourceMappingsError) { + loggerImpl('Update event source mapping failed'); + updateEventSourceCallback(eventSourceMappingsError); + } else { + updateEventSourceCallback(); + } + }); + } + }); + }; + + const updateEventSources = (updateEventSourcesCallback) => { + if (!config.eventSource) { + updateEventSourcesCallback(); + return; + } + + const eventSources = Array.isArray(config.eventSource) + ? config.eventSource : [config.eventSource]; + async.eachSeries( + eventSources, + updateEventSource, + (err) => { + callback(err); + }, + ); + }; + + const updateFunction = (updateFunctionCallback) => { + stat.readFile(codePackage, (err, data) => { + if (err) { + const returnMessage = `Error reading specified package, ${codePackage}`; + return callback(returnMessage); + } + + lambdaNew.updateFunctionCode({ + FunctionName: params.FunctionName, ZipFile: data, Publish: isPublish }, + (updateFunctionCodeError) => { + if (updateFunctionCodeError) { + let warning = 'Package upload failed. '; + warning += 'Check your iam:PassRole permissions.'; + logger(warning); + updateFunctionCallback(updateFunctionCodeError); + } else { + lambdaNew.updateFunctionConfiguration(params, (updateFunctionConfigurationError) => { + if (updateFunctionConfigurationError) { + const warning = 'Update function configuration failed. '; + logger(warning); + updateFunctionCallback(updateFunctionConfigurationError); + } else { + updateEventSources(updateFunctionCallback); + } + }); + } + }); + + return true; + }); + }; + + const createFunction = (createFunctionCallback) => { + stat.readFile(codePackage, (createFunctionError, createFunctionData) => { + if (createFunctionError) { + return callback(`Error reading specified package ${codePackage}`); + } + + params.Code = { ZipFile: createFunctionData }; + params.Runtime = 'runtime' in config ? config.runtime : 'nodejs4.3'; + params.Publish = isPublish; + lambdaNew.createFunction(params, (err) => { + if (err) { + let warning = 'Create function failed. '; + warning += 'Check your iam:PassRole permissions.'; + logger(warning); + createFunctionCallback(err); + } else { + updateEventSources(callback); + } + }); + + return true; + }); + }; + + lambdaNew.getFunction({ FunctionName: params.FunctionName }, (err) => { + if (err) { + if (err.statusCode === 404) { + createFunction(callback); + } else { + let warning = 'AWS API request failed. '; + warning += 'Check your AWS credentials and permissions.'; + logger(warning); + callback(err); + } + } else { + updateFunction(callback); + } + }); +}; diff --git a/test/package.json b/test/package.json new file mode 100644 index 0000000..6ee05ab --- /dev/null +++ b/test/package.json @@ -0,0 +1,43 @@ +{ + "name": "node-aws-lambda", + "version": "0.1.9", + "description": "A module help you automate AWS lambda function deployment.", + "scripts": { + "test": "./node_modules/mocha/bin/mocha test/all.js", + "lint": "./node_modules/.bin/eslint **.js", + "flow": "flow; test $? -eq 0 -o $? -eq 2" + }, + "repository": { + "type": "git", + "url": "https://github.com/ThoughtWorksStudios/node-aws-lambda" + }, + "keywords": [ + "aws", + "lambda", + "deploy" + ], + "author": "Pengchao Wang", + "license": "MIT", + "bugs": { + "url": "https://github.com/ThoughtWorksStudios/node-aws-lambda/issues" + }, + "dependencies": { + "async": "^1.0.0", + "aws-sdk": "2.x.x", + "babel-eslint": "^7.1.1", + "eslint-plugin-flowtype": "^2.25.0", + "fs": "0.0.1-security", + "https-proxy-agent": "^1.0.0", + "loggerpro": "^1.0.2", + "util-extend": "^1.0.3" + }, + "devDependencies": { + "chai": "^2.1.1", + "eslint": "^3.10.2", + "eslint-config-airbnb-base": "^10.0.1", + "eslint-plugin-import": "^2.2.0", + "flow-bin": "^0.35.0", + "mocha": "^2.2.1", + "node-uuid": "^1.4.3" + } +}