From f23aaa1bdeb1ffbf4b863940da7ba9812407ba3f Mon Sep 17 00:00:00 2001 From: motdotla Date: Wed, 20 Jan 2016 07:59:29 -0800 Subject: [PATCH 1/3] Add changelog, and bump version --- CHANGELOG.md | 11 +++++++++++ LICENSE | 24 ++++++++++++++++++++++++ package.json | 4 ++-- test/main.js | 2 +- 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 LICENSE diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..67c71133 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased] + +## [0.8.0] - 2016-01-20 +### Added +- CHANGELOG to ["make it easier for users and contributors to see precisely what notable changes have been made between each release"](http://keepachangelog.com/). Linked to from README +- LICENSE to be more explicit about what was defined in `package.json`. Linked to from README +- it is OK to not set default value for AWS Credentials diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..615b1172 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2016, Scott Motte +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/package.json b/package.json index 3a76904e..303789cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-lambda", - "version": "0.7.1", + "version": "0.8.0", "description": "Command line tool for locally running and remotely deploying your node.js applications to Amazon Lambda.", "main": "lib/main.js", "directories": { @@ -28,7 +28,7 @@ ], "readmeFilename": "README.md", "author": "motdotla", - "license": "BSD", + "license": "BSD-2-Clause", "devDependencies": { "chai": "^2.0.0", "hoek": "^2.11.1", diff --git a/test/main.js b/test/main.js index c0db0185..33a4a1ad 100644 --- a/test/main.js +++ b/test/main.js @@ -33,7 +33,7 @@ describe('node-lambda', function () { }); it('version should be set', function () { - assert.equal(lambda.version, '0.7.1'); + assert.equal(lambda.version, '0.8.0'); }); describe('_params', function () { From 92b8b9400729ebd39c7edd54dab3934bf0d7e53b Mon Sep 17 00:00:00 2001 From: motdotla Date: Wed, 20 Jan 2016 08:00:27 -0800 Subject: [PATCH 2/3] Add node versions to travis --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 77795c6a..26eaf957 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,3 +2,6 @@ language: node_js node_js: - 0.10 + - 0.12 + - 4 + - 5 From c49e0a301007c1e222258c7ce342c4a1a87e905f Mon Sep 17 00:00:00 2001 From: Chase Date: Mon, 18 Apr 2016 13:02:19 +0200 Subject: [PATCH 3/3] Updated changelog (#83) * Added package command to create a local zip. Exclude *.env files from being uploaded to amazon. Exclude .git* to exclude .gitignore. Exclude test from zip. Exclude packageDirectory from zip if specified. * added test for refactored _archive method. * Add -e to not escape \n when echo'ing * VpcConfig support: - http://docs.aws.amazon.com/lambda/latest/dg/API_VpcConfig.html - http://docs.aws.amazon.com/lambda/latest/dg/vpc.html This allows users to place their Lambda functions in specific subnets with specific security groups. * Use AWS API 2015-03-31: - Check if the function exists, if it does update else create new - This API provides compatibility with VpcConfig * Only ignore deploy.env by default * Updated readme: MODE is no longer a thing * Throw on failures * - Fix rsync namespaced error - Add editorconfig - Ignore editorconfig * Update to pass callback for nodejs4.3 runtime (#74) * Update to pass callback for nodejs4.3 runtime * Default to recommended runtime * Add note about runtimes * Added note on how to target v0.10.36 * Runhandler: Hotfixes * Runhandler: Hotfixes (#75) * Post install script second atttempt * Post install script * Add '-x' / '--excludeGlobs' args for general file exclusion on deploy. (2) (#80) This lets me e.g. exclude not only `.env` but by `.env.prod` / `.env.dev` and any other junky / sample files that happen to live in the method's directory. Or for that matter to move all those files into a sub- directory and exclude that. Less policy, more flexibility. * Excludes: Ignore *.swp (#81) * Ignore .swp files by default. Remove redundant deploy.env ignore from the examples. * Add option to create a sample file with a custom filename and fix the run command when running with a custom event file (#51) * Feature/context file (#82) * Add context.json example * Add contextFile -x --contextFile parameter * fix contextFile example filename * update setup log message * Runhandler: Hotfixes * Reformat to match existing whitespace * more reformating to match existing whitespace * revert version back to 0.7.1 * Fix indenting * Update changelog --- .editorconfig | 19 +++ CHANGELOG.md | 14 +- README.md | 50 +++++- bin/node-lambda | 37 ++++- lib/.env.example | 4 + lib/context.json.example | 1 + lib/main.js | 329 ++++++++++++++++++++++++++++++--------- test/main.js | 98 +++++++++++- 8 files changed, 455 insertions(+), 97 deletions(-) create mode 100644 .editorconfig create mode 100644 lib/context.json.example diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..a705a170 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org +root = true + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 67c71133..c0d161f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,18 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] -## [0.8.0] - 2016-01-20 +## [0.8.0] - 2016-04-18 ### Added - CHANGELOG to ["make it easier for users and contributors to see precisely what notable changes have been made between each release"](http://keepachangelog.com/). Linked to from README - LICENSE to be more explicit about what was defined in `package.json`. Linked to from README -- it is OK to not set default value for AWS Credentials +- It is OK to not set default value for AWS Credentials so AWS can use Roles and internally set AWS credentials +- Added `context.json` so it can easily be overwritten +- Allow using a custom (and passed through) `event.json` file +- Added `package` command for easy zip creation and inspection +- Added `VpcConfig` support, see [this PR](https://github.com/motdotla/node-lambda/pull/64) for more information +- Updated the AWS API version used to `2015-03-31` +- Make sure we throw errors on unrecoverable failures so other programs can listen on that +- Added support for nodejs4.3 runtime ([introducted to AWS](https://aws.amazon.com/blogs/compute/node-js-4-3-2-runtime-now-available-on-lambda/) Apr 7 2016) +- Added support for `post install scripts`, this `post_install.sh` file will be triggered after `npm install --production` in case you want to run any code on your application before zipping +- Added `-x` / `--excludeGlobs` to allow custom file exclusion +- Excluding `*.swp`, `deploy.env` by default now diff --git a/README.md b/README.md index b4d45c22..255fa641 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ There are 3 available commands. ``` node-lambda setup node-lambda run +node-lambda package node-lambda deploy ``` @@ -35,7 +36,7 @@ node-lambda deploy #### setup -Initializes the `event.json`, `.env` files, and `deploy.env` files. `event.json` is where you mock your event. `.env` is where you place your deployment configuration. `deploy.env` has the same format as `.env`, but is used for holding any environment/config variables that you need to be deployed with your code to Lambda but you don't want in version control (e.g. DB connection info). +Initializes the `event.json`, `context.json`, `.env` files, and `deploy.env` files. `event.json` is where you mock your event. `context.json` is where you can add additional mock data to the context passed to your lambda function. `.env` is where you place your deployment configuration. `deploy.env` has the same format as `.env`, but is used for holding any environment/config variables that you need to be deployed with your code to Lambda but you don't want in version control (e.g. DB connection info). ``` $ node-lambda setup --help @@ -50,7 +51,7 @@ $ node-lambda setup --help After running setup, it's a good idea to gitignore the generated `event.json` and `.env` files. ``` -echo ".env\ndeploy.env\nevent.json" >> .gitignore +echo -e ".env\ndeploy.env\nevent.json" >> .gitignore ``` #### run @@ -64,9 +65,29 @@ $ node-lambda run --help Options: - -h, --help output usage information - -h, --handler [index.handler] Lambda Handler {index.handler} - -j, --eventFile [event.json] Event JSON File + -h, --help Output usage information + --handler [index.handler] Lambda Handler {index.handler} + -j, --eventFile [event.json] Event JSON File + -u, --runtime [nodejs4.3] Lambda Runtime {nodejs4.3, nodejs} - "nodejs4.3" is the current standard, "nodejs" is v0.10.36 + -x, --contextFile [context.json] Context JSON file +``` + +#### package + +Bundles your application into a local zip file. + +``` +$ node-lambda package --help + + Usage: package [options] + + Options: + + -h, --help output usage information + -p, --packageDirectory [build] Local Package Directory + -n, --functionName [node-lambda] Lambda FunctionName + -e, --environment [staging] Choose environment {development, staging, production} + -f, --configFile [] Path to file holding secret environment variables (e.g. "deploy.env") ``` #### deploy @@ -87,21 +108,34 @@ $ node-lambda deploy --help -k, --sessionToken [your_token] AWS Session Token -r, --region [us-east-1] AWS Region(s) -n, --functionName [node-lambda] Lambda FunctionName - -h, --handler [index.handler] Lambda Handler {index.handler} - -c, --mode [event] Lambda Mode + --handler [index.handler] Lambda Handler {index.handler} -o, --role [your_role] Amazon Role ARN -m, --memorySize [128] Lambda Memory Size -t, --timeout [3] Lambda Timeout -d, --description [missing] Lambda Description - -u, --runtime [nodejs] Lambda Runtime + -u, --runtime [nodejs4.3] Lambda Runtime {nodejs4.3, nodejs} - "nodejs4.3" is the current standard, "nodejs" is v0.10.36 + -p, --publish [false] This boolean parameter can be used to request AWS Lambda to create the Lambda function and publish a version as an atomic operation -v, --version [custom-version] Lambda Version -f, --configFile [] Path to file holding secret environment variables (e.g. "deploy.env")` + -b, --vpcSubnets [] VPC Subnet ID(s, comma separated list) for your Lambda Function, when using this, the below param is also required + -g, --vpcSecurityGroups [] VPC Security Group ID(s, comma separated list) for your Lambda Function, when using this, the above param is also required ``` ## Custom Environment Variables AWS Lambda doesn't let you set environment variables for your function, but in many cases you will need to configure your function with secure values that you don't want to check into version control, for example a DB connection string or encryption key. Use the sample `deploy.env` file in combination with the `--configFile` flag to set values which will be prepended to your compiled Lambda function as `process.env` environment variables before it gets uploaded to S3. +## Node.js Runtime Configuration + +AWS Lambda now supports Node.js v4.3.2, and there have been some [API changes](http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-using-old-runtime.html) for the new version. Most notably, +`context.done()`, `context.succeed()`, and `context.fail()` are deprecated in favor of the Node convention of passing in +a callback function. These will still work for now for backward compatibility, but are no longer recommended. + +v0.10.36 is still supported, and can be targeted by changing the `AWS_RUNTIME` value to `nodejs` in the `.env` file. + +## Post install script +When running `node-lambda deploy` if you need to do some action after `npm install --production` and before deploying to AWS Lambda (i.e. replace some modules with precompiled ones or download some libraries) you can create `post_install.sh` script. If the file exists the script will be executed (and output shown after execution) if not it is skipped. Make sure that the script is executable. + ## Other AWS Lambda Tools Projects + [lambdaws](https://github.com/mentum/lambdaws) diff --git a/bin/node-lambda b/bin/node-lambda index ecbee08c..54be9187 100755 --- a/bin/node-lambda +++ b/bin/node-lambda @@ -9,20 +9,25 @@ dotenv.load(); var AWS_ENVIRONMENT = process.env.AWS_ENVIRONMENT || ''; var CONFIG_FILE = process.env.CONFIG_FILE || ''; +var EXCLUDE_GLOBS = process.env.EXCLUDE_GLOBS || ''; var AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; var AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; var AWS_SESSION_TOKEN = process.env.AWS_SESSION_TOKEN || ''; var AWS_REGION = process.env.AWS_REGION || 'us-east-1,us-west-2,eu-west-1'; var AWS_FUNCTION_NAME = process.env.AWS_FUNCTION_NAME || packageJson.name; var AWS_HANDLER = process.env.AWS_HANDLER || 'index.handler'; -var AWS_MODE = 'event'; var AWS_ROLE = process.env.AWS_ROLE_ARN || process.env.AWS_ROLE || 'missing'; var AWS_MEMORY_SIZE = process.env.AWS_MEMORY_SIZE || 128; var AWS_TIMEOUT = process.env.AWS_TIMEOUT || 60; var AWS_DESCRIPTION = process.env.AWS_DESCRIPTION || ''; -var AWS_RUNTIME = process.env.AWS_RUNTIME || 'nodejs'; +var AWS_RUNTIME = process.env.AWS_RUNTIME || 'nodejs4.3'; +var AWS_PUBLISH = process.env.AWS_PUBLIS || false; var AWS_FUNCTION_VERSION = process.env.AWS_FUNCTION_VERSION || ''; +var AWS_VPC_SUBNETS = process.env.AWS_VPC_SUBNETS || ''; +var AWS_VPC_SECURITY_GROUPS = process.env.AWS_VPC_SECURITY_GROUPS || ''; var EVENT_FILE = process.env.EVENT_FILE || 'event.json'; +var PACKAGE_DIRECTORY = process.env.PACKAGE_DIRECTORY; +var CONTEXT_FILE = process.env.CONTEXT_FILE || 'context.json'; program .version(lambda.version) @@ -35,26 +40,48 @@ program .option('-k, --sessionToken [' + AWS_SESSION_TOKEN + ']', 'AWS Session Token', AWS_SESSION_TOKEN) .option('-r, --region [' + AWS_REGION + ']', 'AWS Region', AWS_REGION) .option('-n, --functionName [' + AWS_FUNCTION_NAME + ']', 'Lambda FunctionName', AWS_FUNCTION_NAME) - .option('-h, --handler [' + AWS_HANDLER + ']', 'Lambda Handler {index.handler}', AWS_HANDLER) - .option('-c, --mode [' + AWS_MODE + ']', 'Lambda Mode', AWS_MODE) + .option('--handler [' + AWS_HANDLER + ']', 'Lambda Handler {index.handler}', AWS_HANDLER) .option('-o, --role [' + AWS_ROLE + ']', 'Amazon Role ARN', AWS_ROLE) .option('-m, --memorySize [' + AWS_MEMORY_SIZE + ']', 'Lambda Memory Size', AWS_MEMORY_SIZE) .option('-t, --timeout [' + AWS_TIMEOUT + ']', 'Lambda Timeout', AWS_TIMEOUT) .option('-d, --description [' + AWS_DESCRIPTION + ']', 'Lambda Description', AWS_DESCRIPTION) .option('-u, --runtime [' + AWS_RUNTIME + ']', 'Lambda Runtime', AWS_RUNTIME) + .option('-p, --publish [' + AWS_PUBLISH + ']', 'Lambda Publish', AWS_PUBLISH) .option('-v, --version [' + AWS_FUNCTION_VERSION + ']', 'Lambda Function Version', AWS_FUNCTION_VERSION) + .option('-b, --vpcSubnets [' + AWS_VPC_SUBNETS + ']', 'Lambda Function VPC Subnets', AWS_VPC_SUBNETS) + .option('-g, --vpcSecurityGroups [' + AWS_VPC_SECURITY_GROUPS + ']', 'Lambda VPC Security Group', + AWS_VPC_SECURITY_GROUPS) + .option('-p, --packageDirectory [' + PACKAGE_DIRECTORY + ']', 'Local Package Directory', PACKAGE_DIRECTORY) .option('-f, --configFile [' + CONFIG_FILE + ']', 'Path to file holding secret environment variables (e.g. "deploy.env")', CONFIG_FILE) + .option('-x, --excludeGlobs [' + EXCLUDE_GLOBS + ']', + 'Space-separated glob pattern(s) for additional exclude files (e.g. "event.json dotenv.sample")', EXCLUDE_GLOBS) .action(function (prg) { lambda.deploy(prg); }); +program + .version(lambda.version) + .command('package') + .description('Create zipped package for Amazon Lambda deployment') + .option('-p, --packageDirectory [' + PACKAGE_DIRECTORY + ']', 'Local Package Directory', PACKAGE_DIRECTORY) + .option('-n, --functionName [' + AWS_FUNCTION_NAME + ']', 'Lambda FunctionName', AWS_FUNCTION_NAME) + .option('-e, --environment [' + AWS_ENVIRONMENT + ']', 'Choose environment {dev, staging, production}', + AWS_ENVIRONMENT) + .option('-f, --configFile [' + CONFIG_FILE + ']', + 'Path to file holding secret environment variables (e.g. "deploy.env")', CONFIG_FILE) + .action(function (prg) { + lambda.package(prg); + }); + program .version(lambda.version) .command('run') .description('Run your Amazon Lambda application locally') - .option('-h, --handler [' + AWS_HANDLER + ']', 'Lambda Handler {index.handler}', AWS_HANDLER) + .option('--handler [' + AWS_HANDLER + ']', 'Lambda Handler {index.handler}', AWS_HANDLER) .option('-j, --eventFile [' + EVENT_FILE + ']', 'Event JSON File', EVENT_FILE) + .option('-u, --runtime [' + AWS_RUNTIME + ']', 'Lambda Runtime', AWS_RUNTIME) + .option('-x, --contextFile [' + CONTEXT_FILE + ']', 'Context JSON File', CONTEXT_FILE) .action(function (prg) { lambda.run(prg); }); diff --git a/lib/.env.example b/lib/.env.example index 7280b4d1..66f81f71 100644 --- a/lib/.env.example +++ b/lib/.env.example @@ -11,3 +11,7 @@ AWS_MEMORY_SIZE=128 AWS_TIMEOUT=3 AWS_DESCRIPTION= AWS_RUNTIME=nodejs +AWS_VPC_SUBNETS= +AWS_VPC_SECURITY_GROUPS= +EXCLUDE_GLOBS="event.json" +PACKAGE_DIRECTORY=build diff --git a/lib/context.json.example b/lib/context.json.example new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/lib/context.json.example @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/lib/main.js b/lib/main.js index 8e147765..81546f5d 100644 --- a/lib/main.js +++ b/lib/main.js @@ -17,8 +17,8 @@ var Lambda = function () { return this; }; -Lambda.prototype._createSampleFile = function (file) { - var exampleFile = process.cwd() + '/' + file; +Lambda.prototype._createSampleFile = function (file, newFileName) { + var exampleFile = process.cwd() + '/' + (file || newFileName); var boilerplateFile = __dirname + '/' + file + '.example'; if (!fs.existsSync(exampleFile)) { @@ -32,55 +32,96 @@ Lambda.prototype.setup = function () { this._createSampleFile('.env'); this._createSampleFile('event.json'); this._createSampleFile('deploy.env'); - console.log('Setup done. Edit the .env, deploy.env, and event.json files as needed.'); + this._createSampleFile('context.json'); + console.log('Setup done. Edit the .env, deploy.env, context.json and event.json files as needed.'); }; Lambda.prototype.run = function (program) { - this._createSampleFile('event.json'); - + this._createSampleFile('event.json', program.eventFile); var splitHandler = program.handler.split('.'); var filename = splitHandler[0] + '.js'; var handlername = splitHandler[1]; var handler = require(process.cwd() + '/' + filename)[handlername]; var event = require(process.cwd() + '/' + program.eventFile); + var context = require(process.cwd() + '/' + program.contextFile); - this._runHandler(handler, event); + this._runHandler(handler, event, program.runtime, context); }; -Lambda.prototype._runHandler = function (handler, event) { - var context = { - succeed: function (result) { - console.log('succeed: ' + JSON.stringify(result)); - process.exit(0); - }, - fail: function (error) { - console.log('fail: ' + error); +Lambda.prototype._runHandler = function (handler, event, runtime, context) { + + var callback = function (err, result) { + if (err) { + console.log('Error: ' + err); process.exit(-1); - }, - done: function () { + } + else { + console.log('Success:'); + if (result) { + console.log(JSON.stringify(result)); + } process.exit(0); } }; - handler(event, context); + var isNode43 = (runtime === "nodejs4.3"); + context.succeed = function (result) { + if (isNode43) { + console.log('context.succeed() is deprecated with Node.js 4.3 runtime'); + } + callback(null, result); + }; + context.fail = function (error) { + if (isNode43) { + console.log('context.fail() is deprecated with Node.js 4.3 runtime'); + } + callback(error); + }; + context.done = function () { + if (isNode43) { + console.log('context.done() is deprecated with Node.js 4.3 runtime'); + } + callback(); + }; + + switch(runtime) { + case "nodejs": + handler(event, context); + break; + case "nodejs4.3": + handler(event, context, callback); + break; + default: + console.error("Runtime [" + runtime + "] is not supported."); + } + }; Lambda.prototype._params = function (program, buffer) { var params = { FunctionName: program.functionName + (program.environment ? '-' + program.environment : ''), - FunctionZip: buffer, + Code: { + ZipFile: buffer + }, Handler: program.handler, - Mode: program.mode, Role: program.role, Runtime: program.runtime, Description: program.description, MemorySize: program.memorySize, - Timeout: program.timeout + Timeout: program.timeout, + Publish: program.publish, + VpcConfig: {} }; if (program.version) { params.FunctionName += ('-' + program.version); } + if (program.vpcSubnets && program.vpcSecurityGroups) { + params.VpcConfig = { + 'SubnetIds': program.vpcSubnets.split(','), + 'SecurityGroupIds': program.vpcSecurityGroups.split(',') + }; + } return params; }; @@ -97,25 +138,66 @@ Lambda.prototype._zipfileTmpPath = function (program) { }; Lambda.prototype._rsync = function (program, codeDirectory, callback) { - exec('rsync -r --exclude=.git --exclude=*.log --exclude=node_modules . ' + codeDirectory, function (err) { + var excludes = ['.git*', '*.swp', '.editorconfig', 'deploy.env', '*.log', 'node_modules'], + excludeArgs = ''; + if (program.excludeGlobs) { + var excludeGlobs = program.excludeGlobs.split(' '); + excludeArgs = excludeGlobs.concat(excludes).map(function(exclude) { + return '--exclude=' + exclude; + }).join(' '); + } + + exec('mkdir -p ' + codeDirectory, function(err) { if (err) { - throw err; + return callback(err); } - return callback(null, true); + exec('rsync -r ' + excludeArgs + ' . ' + codeDirectory, function (err) { + if (err) { + return callback(err); + } + + return callback(null, true); + }); }); }; Lambda.prototype._npmInstall = function (program, codeDirectory, callback) { exec('npm install --production --prefix ' + codeDirectory, function (err) { if (err) { - throw err; + return callback(err); } return callback(null, true); }); }; +Lambda.prototype._postInstallScript = function (codeDirectory, callback) { + var script_filename = 'post_install.sh'; + var cmd = './'+script_filename; + + var filePath = [codeDirectory, script_filename].join('/'); + + fs.exists(filePath, function(exists) { + if (exists) { + console.log('=> Running post install script '+script_filename); + exec(cmd, { cwd: codeDirectory,maxBuffer: 50 * 1024 * 1024 }, function(error, stdout, stderr){ + + if (error) callback(error +" stdout: " + stdout + "stderr"+stderr); + else { + console.log("\t\t"+stdout); + callback(null); + } + }); + + + } else { + callback(null); + } + }); + +}; + Lambda.prototype._zip = function (program, codeDirectory, callback) { var options = { @@ -162,6 +244,22 @@ Lambda.prototype._codeDirectory = function (program) { return os.tmpDir() + '/' + program.functionName + '-' + epoch_time; }; +Lambda.prototype._cleanDirectory = function (codeDirectory, callback) { + exec('rm -rf ' + codeDirectory, function (err) { + if (err) { + throw err; + } + + fs.mkdir(codeDirectory, function(err) { + if (err) { + throw err; + } + + return callback(null, true); + }); + }); +}; + Lambda.prototype._setEnvironmentVars = function (program, codeDirectory) { console.log('=> Setting "environment variables" for Lambda from %s', program.configFile); // Which file is the handler? @@ -185,7 +283,37 @@ Lambda.prototype._setEnvironmentVars = function (program, codeDirectory) { fs.writeFileSync(handlerFileName, prefix + contents.toString()); }; -Lambda.prototype.deploy = function (program) { +Lambda.prototype._uploadExisting = function(lambda, params, cb) { + return lambda.updateFunctionCode({ + 'FunctionName': params.FunctionName, + 'ZipFile': params.Code.ZipFile, + 'Publish': params.publish + }, function(err, data) { + if(err) { + return cb(err, data); + } + + return lambda.updateFunctionConfiguration({ + 'FunctionName': params.FunctionName, + 'Description': params.Description, + 'Handler': params.Handler, + 'MemorySize': params.MemorySize, + 'Role': params.Role, + 'Timeout': params.Timeout, + 'VpcConfig': params.VpcConfig + }, function(err, data) { + return cb(err, data); + }); + }); +}; + +Lambda.prototype._uploadNew = function(lambda, params, cb) { + return lambda.createFunction(params, function(err, data) { + return cb(err, data); + }); +}; + +Lambda.prototype._archive = function (program, archive_callback) { this._createSampleFile('.env'); // Warn if not building on 64-bit linux @@ -197,76 +325,129 @@ Lambda.prototype.deploy = function (program) { } var _this = this; - var regions = program.region.split(','); var codeDirectory = _this._codeDirectory(program); console.log('=> Moving files to temporary directory'); // Move all files to tmp folder (except .git, .log, event.json and node_modules) - - _this._rsync(program, codeDirectory, function (err) { + _this._cleanDirectory(codeDirectory, function(err) { if (err) { - console.error(err); - return; + return archive_callback(err); } - console.log('=> Running npm install --production'); - _this._npmInstall(program, codeDirectory, function (err) { + _this._rsync(program, codeDirectory, function (err) { if (err) { - console.error(err); - return; + return archive_callback(err); } + console.log('=> Running npm install --production'); + _this._npmInstall(program, codeDirectory, function (err) { + if (err) { + return archive_callback(err); + } - // Add custom environment variables if program.configFile is defined - if (program.configFile) { - _this._setEnvironmentVars(program, codeDirectory); - } - console.log('=> Zipping deployment package'); + _this._postInstallScript(codeDirectory, function (err) { + if (err) { + return archive_callback(err); + } - var archive = process.platform !== 'win32' ? _this._nativeZip : _this._zip; - archive = archive.bind(_this); + // Add custom environment variables if program.configFile is defined + if (program.configFile) { + _this._setEnvironmentVars(program, codeDirectory); + } + console.log('=> Zipping deployment package'); - archive(program, codeDirectory, function (err, buffer) { - if (err) { - console.error(err); - return; - } + var archive = process.platform !== 'win32' ? _this._nativeZip : _this._zip; + archive = archive.bind(_this); - console.log('=> Reading zip file to memory'); - var params = _this._params(program, buffer); + archive(program, codeDirectory, archive_callback); + }); + }); + }); + }); +}; - async.map(regions, function (region, cb) { - console.log('=> Uploading zip file to AWS Lambda ' + region + ' with parameters:'); - console.log(params); +Lambda.prototype.package = function (program) { + var _this = this; + if (!program.packageDirectory) { + throw 'packageDirectory not specified!'; + } else { + try { + var isDir = fs.lstatSync(program.packageDirectory).isDirectory(); + + if (!isDir) { + throw program.packageDirectory + ' is not a directory!'; + } + } catch(err) { + if (err.code === 'ENOENT') { + console.log('=> Creating package directory'); + fs.mkdirSync(program.packageDirectory); + } else { + throw err; + } + } + } + + _this._archive(program, function (err, buffer) { + if (err) { + throw err; + } + var basename = program.functionName + (program.environment ? '-' + program.environment : ''); + var zipfile = path.join(program.packageDirectory, basename + '.zip'); + console.log('=> Writing packaged zip'); + fs.writeFile(zipfile, buffer, function(err) { + if (err) { + throw err; + } + console.log('Packaged zip created: ' + zipfile); + }); + }); +}; + +Lambda.prototype.deploy = function (program) { + var _this = this; + var regions = program.region.split(','); + _this._archive(program, function (err, buffer) { + if (err) { + throw err; + } - var aws_security = { - accessKeyId: program.accessKey, - secretAccessKey: program.secretKey, - region: region - }; + console.log('=> Reading zip file to memory'); + var params = _this._params(program, buffer); - if (program.sessionToken){ - aws_security.sessionToken = program.sessionToken; - }; + async.map(regions, function (region, cb) { + console.log('=> Uploading zip file to AWS Lambda ' + region + ' with parameters:'); + console.log(params); - aws.config.update(aws_security); + var aws_security = { + accessKeyId: program.accessKey, + secretAccessKey: program.secretKey, + region: region + }; - var lambda = new aws.Lambda({ - apiVersion: '2014-11-11' - }); + if (program.sessionToken){ + aws_security.sessionToken = program.sessionToken; + } - lambda.uploadFunction(params, function (err, data) { - cb(err, data); - }); + aws.config.update(aws_security); - }, function (err, results) { - if (err) { - console.error(err); - } else { - console.log('=> Zip file(s) done uploading. Results follow: '); - console.log(results); - } - }); + var lambda = new aws.Lambda({ + apiVersion: '2015-03-31' + }); + + return lambda.getFunction({ + 'FunctionName': params.FunctionName + }, function(err) { + if(err) { + return _this._uploadNew(lambda, params, cb); + } + return _this._uploadExisting(lambda, params, cb); }); + }, function (err, results) { + if (err) { + throw err; + } else { + console.log('=> Zip file(s) done uploading. Results follow: '); + console.log(results); + } }); }); }; diff --git a/test/main.js b/test/main.js index 33a4a1ad..7d779783 100644 --- a/test/main.js +++ b/test/main.js @@ -16,7 +16,6 @@ var originalProgram = { sessionToken: 'token', functionName: 'node-lambda', handler: 'index.handler', - mode: 'event', role: 'some:arn:aws:iam::role', memorySize: 128, timeout: 3, @@ -53,6 +52,23 @@ describe('node-lambda', function () { var params = lambda._params(program); assert.equal(params.FunctionName, 'node-lambda-development-2015-02-01'); }); + + it('appends VpcConfig to params when vpc params set', function() { + program.vpcSubnets = 'subnet-00000000,subnet-00000001,subnet-00000002'; + program.vpcSecurityGroups = 'sg-00000000,sg-00000001,sg-00000002'; + var params = lambda._params(program); + assert.equal(params.VpcConfig.SubnetIds[0], program.vpcSubnets.split(',')[0]); + assert.equal(params.VpcConfig.SubnetIds[1], program.vpcSubnets.split(',')[1]); + assert.equal(params.VpcConfig.SubnetIds[2], program.vpcSubnets.split(',')[2]); + assert.equal(params.VpcConfig.SecurityGroupIds[0], program.vpcSecurityGroups.split(',')[0]); + assert.equal(params.VpcConfig.SecurityGroupIds[1], program.vpcSecurityGroups.split(',')[1]); + assert.equal(params.VpcConfig.SecurityGroupIds[2], program.vpcSecurityGroups.split(',')[2]); + }); + + it('does not append VpcConfig when params are not set', function() { + var params = lambda._params(program); + assert.equal(Object.keys(params.VpcConfig).length, 0); + }); }); describe('_zipfileTmpPath', function () { @@ -64,25 +80,67 @@ describe('node-lambda', function () { }); describe('_rsync', function () { + beforeEach(function (done) { + lambda._cleanDirectory(codeDirectory, done); + }); + it('rsync an index.js as well as other files', function (done) { lambda._rsync(program, codeDirectory, function (err, result) { var contents = fs.readdirSync(codeDirectory); - result = _.includes(contents, 'index.js'); + result = _.includes(contents, 'index.js') || + _.includes(contents, 'package.json'); assert.equal(result, true); done(); }); }); + + describe("when there are excluded files", function (done) { + beforeEach(function (done) { + program.excludeGlobs="*.png test" + done(); + }); + + it('rsync an index.js as well as other files', function (done) { + lambda._rsync(program, codeDirectory, function (err, result) { + var contents = fs.readdirSync(codeDirectory); + + result = _.includes(contents, 'index.js') || + _.includes(contents, 'package.json'); + assert.equal(result, true); + + done(); + }); + }); + + it('rsync excludes files matching excludeGlobs', function (done) { + lambda._rsync(program, codeDirectory, function (err, result) { + var contents = fs.readdirSync(codeDirectory); + + result = _.includes(contents, 'node-lambda.png') || + _.includes(contents, 'test'); + assert.equal(result, false); + + done(); + }); + }); + }); }); describe('_npmInstall', function () { beforeEach(function (done) { - lambda._rsync(program, codeDirectory, function (err) { + lambda._cleanDirectory(codeDirectory, function (err) { if (err) { return done(err); } - done(); + + lambda._rsync(program, codeDirectory, function (err) { + if (err) { + return done(err); + } + done(); + }); }); }); @@ -92,7 +150,7 @@ describe('node-lambda', function () { lambda._npmInstall(program, codeDirectory, function (err, result) { var contents = fs.readdirSync(codeDirectory); - result = _.includes(contents, 'index.js'); + result = _.includes(contents, 'node_modules'); assert.equal(result, true); done(); @@ -103,15 +161,21 @@ describe('node-lambda', function () { describe('_zip', function () { beforeEach(function (done) { this.timeout(30000); // give it time to build the node modules - lambda._rsync(program, codeDirectory, function (err) { + lambda._cleanDirectory(codeDirectory, function (err) { if (err) { return done(err); } - lambda._npmInstall(program, codeDirectory, function (err) { + + lambda._rsync(program, codeDirectory, function (err) { if (err) { return done(err); } - done(); + lambda._npmInstall(program, codeDirectory, function (err) { + if (err) { + return done(err); + } + done(); + }); }); }); }); @@ -132,6 +196,24 @@ describe('node-lambda', function () { }); }); + describe('_archive', function () { + it('installs and zips with an index.js file and node_modules/async', function (done) { + this.timeout(30000); // give it time to zip + + lambda._archive(program, function (err, data) { + var archive = new zip(data); + var contents = _.map(archive.files, function (f) { + return f.name.toString(); + }); + var result = _.includes(contents, 'index.js'); + assert.equal(result, true); + result = _.includes(contents, 'node_modules/async/lib/async.js'); + assert.equal(result, true); + done(); + }) + }) + }); + describe('environment variable injection', function () { beforeEach(function () { // Prep...