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...