diff --git a/README.md b/README.md index cec1a5b..95c1dd6 100644 --- a/README.md +++ b/README.md @@ -435,6 +435,32 @@ A list of one or more security groups IDs in your VPC. If your Lambda function accesses resources in a VPC you must provide at least one security group and one subnet ID. These must belong to the same VPC +##### options.s3_bucket +Type: `String` +Default value: `undefined` + +An optional S3 bucket that contains a pre uploaded package for deployment. + +If this property is specified, the task will attempt to load the package from the specified S3 bucket, instead of uploading it from the file system. + +The `package` property is used to determine the name of the file within the bucket. + +##### options.s3_path +Type: `String` +Default value: `undefined` + +An optional S3 path that points to the "directory" within S3 that contains the package. If omitted, the task assumes that the package has been deployed at the root of the S3 bucket. + +The `package` property is used to determine the name of the file within the bucket. + +##### options.s3_version +Type: `String` +Default value: `undefined` + +An optional S3 version id string that can be used to specifiy a specific version of the package file on s3. If omitted, the latest version of the package will be used. + +The `package` property is used to determine the name of the file within the bucket. + #### Usage Examples ##### Default Options @@ -643,4 +669,4 @@ Adding more warnings for various failure cases * Added support for Node 4.3 runtime callback function - [pull request by bobhigs](https://github.com/Tim-B/grunt-aws-lambda/pull/76) * Added VPC support - [pull request by beeva-arturomartinez](https://github.com/Tim-B/grunt-aws-lambda/pull/71) -* Added local proxy support - [pull request by alekstr](https://github.com/Tim-B/grunt-aws-lambda/pull/66) \ No newline at end of file +* Added local proxy support - [pull request by alekstr](https://github.com/Tim-B/grunt-aws-lambda/pull/66) diff --git a/test/unit/date_facade_test.js b/test/unit/date_facade_test.js index 45e710d..886adef 100644 --- a/test/unit/date_facade_test.js +++ b/test/unit/date_facade_test.js @@ -40,7 +40,8 @@ dateFacadeTest.testGetFormattedTimestamp = function(test) { dateFacadeTest.testGetHumanReadableTimestamp = function(test) { var fixedDate = new Date(2016, 2, 13, 14, 38, 13); - test.ok(dateFacade.getHumanReadableTimestamp(fixedDate).indexOf('Sun Mar 13 2016 14:38:13') > -1); + var expectedValue = fixedDate.toLocaleString(); + test.ok(dateFacade.getHumanReadableTimestamp(fixedDate)===expectedValue); test.done(); }; module.exports = dateFacadeTest; diff --git a/test/unit/deploy_task_test.js b/test/unit/deploy_task_test.js index d584f18..52eb08f 100644 --- a/test/unit/deploy_task_test.js +++ b/test/unit/deploy_task_test.js @@ -188,6 +188,140 @@ deployTaskTest.testDeployWithoutProxy = function(test) { gruntMock.execute(deployTask.getHandler, harnessParams); }; +deployTaskTest.testDeployS3 = function(test) { + test.expect(9); + + var deployTask = require('../../utils/deploy_task'); + var expectedPackage = 'my-package.zip'; + var expectedS3Bucket = 'my-s3-bucket'; + var expectedS3Path = 'my-s3-path'; + var expectedS3Key = expectedS3Path + '/' + expectedPackage; + var progressMessage = [ + 'Using code deployed to S3 at [', + expectedS3Bucket, '/', expectedS3Key, + '] (version: LATEST)' + ].join(''); + + var harnessParams = { + options: { }, + config: { + 'lambda_deploy.fake-target.function': 'some-function', + 'lambda_deploy.fake-target.package': expectedPackage, + 'lambda_deploy.fake-target.s3_bucket': expectedS3Bucket, + 'lambda_deploy.fake-target.s3_path': expectedS3Path + }, + callback: function(harness) { + test.equal(harness.status, true); + test.equal(harness.output.length, 3); + test.equal(harness.output[0], progressMessage); + test.equal(harness.output[1], 'Package deployed.'); + test.equal(harness.output[2], 'No config updates to make.'); + + test.ok(awsSDKMock.config.update.calledWith({region: 'us-east-1'})); + test.ok(lambdaAPIMock.getFunction.calledWith({FunctionName: 'some-function'})); + test.ok(lambdaAPIMock.updateFunctionCode.calledWith({ + FunctionName: 'some-function', + S3Bucket: expectedS3Bucket, + S3Key: expectedS3Key + })); + test.ok(!lambdaAPIMock.updateFunctionConfiguration.called); + test.done(); + } + }; + gruntMock.execute(deployTask.getHandler, harnessParams); +}; + +deployTaskTest.testDeployS3WithoutPath = function(test) { + test.expect(9); + + var deployTask = require('../../utils/deploy_task'); + var expectedPackage = 'my-package.zip'; + var expectedS3Bucket = 'my-s3-bucket'; + var expectedS3Path = ''; + var expectedS3Key = expectedPackage; + var progressMessage = [ + 'Using code deployed to S3 at [', + expectedS3Bucket, '/', expectedPackage, + '] (version: LATEST)' + ].join(''); + + var harnessParams = { + options: { }, + config: { + 'lambda_deploy.fake-target.function': 'some-function', + 'lambda_deploy.fake-target.package': expectedPackage, + 'lambda_deploy.fake-target.s3_bucket': expectedS3Bucket, + 'lambda_deploy.fake-target.s3_path': expectedS3Path + }, + callback: function(harness) { + test.equal(harness.status, true); + test.equal(harness.output.length, 3); + test.equal(harness.output[0], progressMessage); + test.equal(harness.output[1], 'Package deployed.'); + test.equal(harness.output[2], 'No config updates to make.'); + + test.ok(awsSDKMock.config.update.calledWith({region: 'us-east-1'})); + test.ok(lambdaAPIMock.getFunction.calledWith({FunctionName: 'some-function'})); + test.ok(lambdaAPIMock.updateFunctionCode.calledWith({ + FunctionName: 'some-function', + S3Bucket: expectedS3Bucket, + S3Key: expectedS3Key + })); + test.ok(!lambdaAPIMock.updateFunctionConfiguration.called); + test.done(); + } + }; + gruntMock.execute(deployTask.getHandler, harnessParams); +}; + +deployTaskTest.testDeployS3WithVersion = function(test) { + test.expect(9); + + var deployTask = require('../../utils/deploy_task'); + var expectedPackage = 'my-package.zip'; + var expectedS3Bucket = 'my-s3-bucket'; + var expectedS3Path = 'my-s3-path'; + var expectedS3Version = 'my-version'; + var expectedS3Key = expectedS3Path + '/' + expectedPackage; + var progressMessage = [ + 'Using code deployed to S3 at [', + expectedS3Bucket, '/', expectedS3Key, + '] (version: ', + expectedS3Version, + ')' + ].join(''); + + var harnessParams = { + options: { }, + config: { + 'lambda_deploy.fake-target.function': 'some-function', + 'lambda_deploy.fake-target.package': expectedPackage, + 'lambda_deploy.fake-target.s3_bucket': expectedS3Bucket, + 'lambda_deploy.fake-target.s3_path': expectedS3Path, + 'lambda_deploy.fake-target.s3_version': expectedS3Version + }, + callback: function(harness) { + test.equal(harness.status, true); + test.equal(harness.output.length, 3); + test.equal(harness.output[0], progressMessage); + test.equal(harness.output[1], 'Package deployed.'); + test.equal(harness.output[2], 'No config updates to make.'); + + test.ok(awsSDKMock.config.update.calledWith({region: 'us-east-1'})); + test.ok(lambdaAPIMock.getFunction.calledWith({FunctionName: 'some-function'})); + test.ok(lambdaAPIMock.updateFunctionCode.calledWith({ + FunctionName: 'some-function', + S3Bucket: expectedS3Bucket, + S3Key: expectedS3Key, + S3ObjectVersion: expectedS3Version + })); + test.ok(!lambdaAPIMock.updateFunctionConfiguration.called); + test.done(); + } + }; + gruntMock.execute(deployTask.getHandler, harnessParams); +}; + deployTaskTest.testProfile = function(test) { test.expect(3); diff --git a/test/unit/invoke_task_test.js b/test/unit/invoke_task_test.js index e116477..e404c6e 100644 --- a/test/unit/invoke_task_test.js +++ b/test/unit/invoke_task_test.js @@ -422,6 +422,81 @@ invokeTaskTests.testNoClientContext = function(test) { gruntMock.execute(invokeTask.getHandler, harnessParams); }; +invokeTaskTests.testNoFunctionName = function(test) { + test.expect(5); + + setLambdaFunction(function(event, context) { + var defaultFunctionName = '_function_name_'; + test.equal(context.functionName, defaultFunctionName); + context.done(null, 'My Message'); + }); + + var invokeTask = require('../../utils/invoke_task'); + + var harnessParams = { + options: { + }, + callback: function(harness) { + test.equal(harness.status, true); + test.equal(harness.output.length, 5); + test.equal(harness.output[2], 'Success! Message:'); + test.equal(harness.output[4], 'My Message'); + test.done(); + } + }; + gruntMock.execute(invokeTask.getHandler, harnessParams); +}; + +invokeTaskTests.testNoFunctionVersion = function(test) { + test.expect(5); + + setLambdaFunction(function(event, context) { + var defaultFunctionVersion = 1; + test.equal(context.functionVersion, defaultFunctionVersion); + context.done(null, 'My Message'); + }); + + var invokeTask = require('../../utils/invoke_task'); + + var harnessParams = { + options: { + }, + callback: function(harness) { + test.equal(harness.status, true); + test.equal(harness.output.length, 5); + test.equal(harness.output[2], 'Success! Message:'); + test.equal(harness.output[4], 'My Message'); + test.done(); + } + }; + gruntMock.execute(invokeTask.getHandler, harnessParams); +}; + +invokeTaskTests.testNoInvokedFunctionArn = function(test) { + test.expect(5); + + setLambdaFunction(function(event, context) { + var defaultArn = 'arn:aws:lambda:_aws_region_:_aws_account_id_:function:_lambda_function_name_'; + test.equal(context.invokedFunctionArn, defaultArn); + context.done(null, 'My Message'); + }); + + var invokeTask = require('../../utils/invoke_task'); + + var harnessParams = { + options: { + }, + callback: function(harness) { + test.equal(harness.status, true); + test.equal(harness.output.length, 5); + test.equal(harness.output[2], 'Success! Message:'); + test.equal(harness.output[4], 'My Message'); + test.done(); + } + }; + gruntMock.execute(invokeTask.getHandler, harnessParams); +}; + invokeTaskTests.testNoIdentity = function(test) { test.expect(5); @@ -537,4 +612,4 @@ invokeTaskTests.testCallbackError = function(test) { }; -module.exports = invokeTaskTests; \ No newline at end of file +module.exports = invokeTaskTests; diff --git a/utils/deploy_task.js b/utils/deploy_task.js index fd6c675..ce0269c 100644 --- a/utils/deploy_task.js +++ b/utils/deploy_task.js @@ -80,6 +80,9 @@ deployTask.getHandler = function (grunt) { var deploy_function = grunt.config.get('lambda_deploy.' + this.target + '.function'); var deploy_arn = grunt.config.get('lambda_deploy.' + this.target + '.arn'); var deploy_package = grunt.config.get('lambda_deploy.' + this.target + '.package'); + var s3_bucket = grunt.config.get('lambda_deploy.' + this.target + '.s3_bucket'); + var s3_path = grunt.config.get('lambda_deploy.' + this.target + '.s3_path'); + var s3_version = grunt.config.get('lambda_deploy.' + this.target + '.s3_version'); var package_version = grunt.config.get('lambda_deploy.' + this.target + '.version'); var package_name = grunt.config.get('lambda_deploy.' + this.target + '.package_name'); var archive_name = grunt.config.get('lambda_deploy.' + this.target + '.archive_name'); @@ -248,36 +251,68 @@ deployTask.getHandler = function (grunt) { } }; - grunt.log.writeln('Uploading...'); - fs.readFile(deploy_package, function (err, data) { - if (err) { - grunt.fail.warn('Could not read package file (' + deploy_package + '), verify the lambda package ' + - 'location is correct, and that you have already created the package using lambda_package.'); - } + var updateLambdaConfig = function(functionName, configParams) { + updateConfig(functionName, configParams) + .then(function () {return createVersion(functionName);}) + .then(function () {return setAliases(functionName);}) + .then(function () {return setPackageVersionAlias(functionName);}) + .then(function () { + done(true); + }).catch(function (err) { + grunt.fail.warn('Uncaught exception: ' + err.message); + }); + }; + + if(!s3_bucket) { + grunt.log.writeln('Uploading...'); + fs.readFile(deploy_package, function (err, data) { + if (err) { + grunt.fail.warn('Could not read package file (' + deploy_package + '), verify the lambda package ' + + 'location is correct, and that you have already created the package using lambda_package.'); + } + + var codeParams = { + FunctionName: deploy_function, + ZipFile: data + }; + + lambda.updateFunctionCode(codeParams, function (err, data) { + if (err) { + grunt.fail.warn('Package upload failed, check you have lambda:UpdateFunctionCode permissions and that your package is not too big to upload.'); + } + + grunt.log.writeln('Package deployed.'); + updateLambdaConfig(deploy_function, configParams); + }); + }); + } else { + var s3_key = !s3_path? deploy_package:s3_path + '/' + deploy_package; + grunt.log.writeln([ + 'Using code deployed to S3 at [', + s3_bucket, '/', s3_key, + '] (version: ', + s3_version || 'LATEST', + ')' ].join('')); var codeParams = { FunctionName: deploy_function, - ZipFile: data + S3Bucket: s3_bucket, + S3Key: s3_key }; + if(s3_version) { + codeParams.S3ObjectVersion = s3_version; + } + lambda.updateFunctionCode(codeParams, function (err, data) { if (err) { - grunt.fail.warn('Package upload failed, check you have lambda:UpdateFunctionCode permissions and that your package is not too big to upload.'); + grunt.fail.warn('Package deployment failed, check you have lambda:UpdateFunctionCode permissions and that your package is not too big to upload.'); } - grunt.log.writeln('Package deployed.'); - updateConfig(deploy_function, configParams) - .then(function () {return createVersion(deploy_function);}) - .then(function () {return setAliases(deploy_function);}) - .then(function () {return setPackageVersionAlias(deploy_function);}) - .then(function () { - done(true); - }).catch(function (err) { - grunt.fail.warn('Uncaught exception: ' + err.message); - }); + updateLambdaConfig(deploy_function, configParams); }); - }); + } }); }; }; diff --git a/utils/invoke_task.js b/utils/invoke_task.js index a9db0b9..014b74d 100644 --- a/utils/invoke_task.js +++ b/utils/invoke_task.js @@ -27,6 +27,9 @@ invokeTask.getHandler = function (grunt) { 'file_name': 'index.js', 'event': 'event.json', 'client_context': 'client_context.json', + 'invokedFunctionArn': 'arn:aws:lambda:_aws_region_:_aws_account_id_:function:_lambda_function_name_', + 'functionName': '_function_name_', + 'functionVersion': 1, 'identity': 'identity.json' }); @@ -93,6 +96,9 @@ invokeTask.getHandler = function (grunt) { }, awsRequestId: 'LAMBDA_INVOKE', logStreamName: 'LAMBDA_INVOKE', + invokedFunctionArn: options.invokedFunctionArn, + functionName: options.functionName, + functionVersion: options.functionVersion, clientContext: clientContext, identity: identity }; @@ -107,4 +113,4 @@ invokeTask.getHandler = function (grunt) { }; }; -module.exports = invokeTask; \ No newline at end of file +module.exports = invokeTask;