From 2ad01b3fe362ba296bfd9ddbb6405abb3bb1e8ab Mon Sep 17 00:00:00 2001 From: Daniel Schep Date: Tue, 27 Nov 2018 14:09:46 -0500 Subject: [PATCH] [WIP] Support for publishing Lambda Layers --- lib/classes/Service.js | 15 + lib/plugins/Plugins.json | 1 + lib/plugins/aws/deploy/lib/uploadArtifacts.js | 15 +- .../aws/deploy/lib/uploadArtifacts.test.js | 21 +- lib/plugins/aws/lib/naming.js | 7 + .../aws/package/compile/functions/index.js | 4 + .../aws/package/compile/layers/index.js | 85 + .../aws/package/compile/layers/index.test.js | 2147 +++++++++++++++++ lib/plugins/aws/package/index.js | 3 + .../lib/generateArtifactDirectoryName.js | 15 +- lib/plugins/package/lib/packageService.js | 54 +- lib/plugins/package/lib/zipService.js | 8 +- lib/plugins/package/package.js | 1 + 13 files changed, 2348 insertions(+), 28 deletions(-) create mode 100644 lib/plugins/aws/package/compile/layers/index.js create mode 100644 lib/plugins/aws/package/compile/layers/index.test.js diff --git a/lib/classes/Service.js b/lib/classes/Service.js index 7b95f4afa..293b42954 100644 --- a/lib/classes/Service.js +++ b/lib/classes/Service.js @@ -156,6 +156,10 @@ class Service { that.package.excludeDevDependencies = serverlessFile.package.excludeDevDependencies; } + if (that.provider.name === 'aws') { + that.layers = serverlessFile.layers || {}; + } + return this; } @@ -223,6 +227,10 @@ class Service { return Object.keys(this.functions); } + getAllLayers() { + return this.layers ? Object.keys(this.layers) : []; + } + getAllFunctionsNames() { return this.getAllFunctions().map((func) => this.getFunction(func).name); } @@ -234,6 +242,13 @@ class Service { throw new ServerlessError(`Function "${functionName}" doesn't exist in this Service`); } + getLayer(layerName) { + if (layerName in this.layers) { + return this.layers[layerName]; + } + throw new ServerlessError(`Layer "${layerName}" doesn't exist in this Service`); + } + getEventInFunction(eventName, functionName) { const event = this.getFunction(functionName).events .find(e => Object.keys(e)[0] === eventName); diff --git a/lib/plugins/Plugins.json b/lib/plugins/Plugins.json index 5ad2cb348..ca0a82c5a 100644 --- a/lib/plugins/Plugins.json +++ b/lib/plugins/Plugins.json @@ -33,6 +33,7 @@ "./aws/rollback/index.js", "./aws/rollbackFunction/index.js", "./aws/package/compile/functions/index.js", + "./aws/package/compile/layers/index.js", "./aws/package/compile/events/schedule/index.js", "./aws/package/compile/events/s3/index.js", "./aws/package/compile/events/apiGateway/index.js", diff --git a/lib/plugins/aws/deploy/lib/uploadArtifacts.js b/lib/plugins/aws/deploy/lib/uploadArtifacts.js index ed9d2e100..e469737c9 100644 --- a/lib/plugins/aws/deploy/lib/uploadArtifacts.js +++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.js @@ -16,7 +16,7 @@ module.exports = { uploadArtifacts() { return BbPromise.bind(this) .then(this.uploadCloudFormationFile) - .then(this.uploadFunctions); + .then(this.uploadFunctionsAndLayers); }, uploadCloudFormationFile() { @@ -78,7 +78,7 @@ module.exports = { params); }, - uploadFunctions() { + uploadFunctionsAndLayers() { this.serverless.cli.log('Uploading artifacts...'); const functionNames = this.serverless.service.getAllFunctions(); @@ -103,6 +103,17 @@ module.exports = { }) ); + const layerNames = this.serverless.service.getAllLayers(); + artifactFilePaths.push(..._.map(layerNames, (name) => { + const layerObject = this.serverless.service.getLayer(name); + + if (layerObject.package && layerObject.package.artifact) { + return layerObject.package.artifact; + } + + return path.join(this.packagePath, this.provider.naming.getLayerArtifactName(name)); + })); + return BbPromise.map(artifactFilePaths, (artifactFilePath) => { const stats = fs.statSync(artifactFilePath); this.serverless.cli.log(`Uploading service .zip file to S3 (${filesize(stats.size)})...`); diff --git a/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js b/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js index a54249fe6..c42ff02a8 100644 --- a/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js +++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js @@ -61,17 +61,18 @@ describe('uploadArtifacts', () => { it('should run promise chain in order', () => { const uploadCloudFormationFileStub = sinon .stub(awsDeploy, 'uploadCloudFormationFile').resolves(); - const uploadFunctionsStub = sinon - .stub(awsDeploy, 'uploadFunctions').resolves(); + const uploadFunctionsAndLayersStub = sinon + .stub(awsDeploy, 'uploadFunctionsAndLayers').resolves(); return awsDeploy.uploadArtifacts().then(() => { expect(uploadCloudFormationFileStub.calledOnce) .to.be.equal(true); - expect(uploadFunctionsStub.calledAfter(uploadCloudFormationFileStub)).to.be.equal(true); + expect(uploadFunctionsAndLayersStub.calledAfter(uploadCloudFormationFileStub)).to.be.equal( + true); awsDeploy.uploadCloudFormationFile.restore(); - awsDeploy.uploadFunctions.restore(); + awsDeploy.uploadFunctionsAndLayers.restore(); }); }); }); @@ -224,7 +225,7 @@ describe('uploadArtifacts', () => { }); }); - describe('#uploadFunctions()', () => { + describe('#uploadFunctionsAndLayers()', () => { let uploadZipFileStub; beforeEach(() => { @@ -241,7 +242,7 @@ describe('uploadArtifacts', () => { awsDeploy.serverless.config.servicePath = 'some/path'; awsDeploy.serverless.service.service = 'new-service'; - return awsDeploy.uploadFunctions().then(() => { + return awsDeploy.uploadFunctionsAndLayers().then(() => { expect(uploadZipFileStub.calledOnce).to.be.equal(true); const expectedPath = path.join('foo', '.serverless', 'new-service.zip'); expect(uploadZipFileStub.args[0][0]).to.be.equal(expectedPath); @@ -262,7 +263,7 @@ describe('uploadArtifacts', () => { }, }; - return awsDeploy.uploadFunctions().then(() => { + return awsDeploy.uploadFunctionsAndLayers().then(() => { expect(uploadZipFileStub.calledOnce).to.be.equal(true); expect(uploadZipFileStub.args[0][0]).to.be.equal('artifact.zip'); }); @@ -283,7 +284,7 @@ describe('uploadArtifacts', () => { }, }; - return awsDeploy.uploadFunctions().then(() => { + return awsDeploy.uploadFunctionsAndLayers().then(() => { expect(uploadZipFileStub.calledTwice).to.be.equal(true); expect(uploadZipFileStub.args[0][0]) .to.be.equal(awsDeploy.serverless.service.functions.first.package.artifact); @@ -307,7 +308,7 @@ describe('uploadArtifacts', () => { }, }; - return awsDeploy.uploadFunctions().then(() => { + return awsDeploy.uploadFunctionsAndLayers().then(() => { expect(uploadZipFileStub.calledTwice).to.be.equal(true); expect(uploadZipFileStub.args[0][0]) .to.be.equal(awsDeploy.serverless.service.functions.first.package.artifact); @@ -322,7 +323,7 @@ describe('uploadArtifacts', () => { sinon.spy(awsDeploy.serverless.cli, 'log'); - return awsDeploy.uploadFunctions().then(() => { + return awsDeploy.uploadFunctionsAndLayers().then(() => { const expected = 'Uploading service .zip file to S3 (1 KB)...'; expect(awsDeploy.serverless.cli.log.calledWithExactly(expected)).to.be.equal(true); }); diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js index 8e25ec3c5..385c4c4b8 100644 --- a/lib/plugins/aws/lib/naming.js +++ b/lib/plugins/aws/lib/naming.js @@ -61,6 +61,10 @@ module.exports = { return `${functionName}.zip`; }, + getLayerArtifactName(layerName) { + return `${layerName}.zip`; + }, + getServiceStateFileName() { return 'serverless-state.json'; }, @@ -136,6 +140,9 @@ module.exports = { getLambdaLogicalId(functionName) { return `${this.getNormalizedFunctionName(functionName)}LambdaFunction`; }, + getLambdaLayerLogicalId(functionName) { + return `${this.getNormalizedFunctionName(functionName)}LambdaLayer`; + }, getLambdaLogicalIdRegex() { return /LambdaFunction$/; }, diff --git a/lib/plugins/aws/package/compile/functions/index.js b/lib/plugins/aws/package/compile/functions/index.js index 00826d8c1..3d662abb7 100644 --- a/lib/plugins/aws/package/compile/functions/index.js +++ b/lib/plugins/aws/package/compile/functions/index.js @@ -316,6 +316,10 @@ class AwsCompileFunctions { if (functionObject.layers && _.isArray(functionObject.layers)) { newFunction.Properties.Layers = functionObject.layers; + /* TODO - is a DependsOn needed? + newLayer.DependsOn = [NEW LAYER??] + .concat(newLayer.DependsOn || []); + */ } const functionLogicalId = this.provider.naming diff --git a/lib/plugins/aws/package/compile/layers/index.js b/lib/plugins/aws/package/compile/layers/index.js new file mode 100644 index 000000000..0e43638fd --- /dev/null +++ b/lib/plugins/aws/package/compile/layers/index.js @@ -0,0 +1,85 @@ +'use strict'; + +const BbPromise = require('bluebird'); +const _ = require('lodash'); +const path = require('path'); + +class AwsCompileLayers { + constructor(serverless, options) { + this.serverless = serverless; + this.options = options; + const servicePath = this.serverless.config.servicePath || ''; + this.packagePath = this.serverless.service.package.path || + path.join(servicePath || '.', '.serverless'); + + this.provider = this.serverless.getProvider('aws'); + + this.hooks = { + 'package:compileLayers': () => BbPromise.bind(this) + .then(this.compileLayers), + }; + } + + compileLayer(layerName) { + const newLayer = this.cfLambdaLayerTemplate(); + const layerObject = this.serverless.service.getLayer(layerName); + layerObject.package = layerObject.package || {}; + + const artifactFileName = this.provider.naming.getLayerArtifactName(layerName); + const artifactFilePath = layerObject.package && layerObject.package.artifact + ? layerObject.package.artifact + : path.join(this.serverless.config.servicePath, '.serverless', artifactFileName); + + if (this.serverless.service.package.deploymentBucket) { + newLayer.Properties.Content.S3Bucket = this.serverless.service.package.deploymentBucket; + } + + const s3Folder = this.serverless.service.package.artifactDirectoryName; + const s3FileName = artifactFilePath.split(path.sep).pop(); + newLayer.Properties.Content.S3Key = `${s3Folder}/${s3FileName}`; + + newLayer.Properties.LayerName = layerObject.name || layerName; + if (layerObject.description) { + newLayer.Properties.Description = layerObject.description; + } + if (layerObject.licenseInfo) { + newLayer.Properties.LicenseInfo = layerObject.licenseInfo; + } + if (layerObject.compatibleRuntimes) { + newLayer.Properties.CompatibleRuntimes = layerObject.compatibleRuntimes; + } + + const layerLogicalId = this.provider.naming.getLambdaLayerLogicalId(layerName); + const newLayerObject = { + [layerLogicalId]: newLayer, + }; + + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, + newLayerObject); + } + + compileLayers() { + const allLayers = this.serverless.service.getAllLayers(); + return BbPromise.each( + allLayers, + layerName => this.compileLayer(layerName) + ); + } + + cfLambdaLayerTemplate() { + return { + Type: 'AWS::Lambda::LayerVersion', + Properties: { + Content: { + S3Bucket: { + Ref: 'ServerlessDeploymentBucket', + }, + S3Key: 'S3Key', + }, + LayerName: 'LayerName', + }, + }; + } +} + +module.exports = AwsCompileLayers; diff --git a/lib/plugins/aws/package/compile/layers/index.test.js b/lib/plugins/aws/package/compile/layers/index.test.js new file mode 100644 index 000000000..461d3e2f3 --- /dev/null +++ b/lib/plugins/aws/package/compile/layers/index.test.js @@ -0,0 +1,2147 @@ +'use strict'; + +// eslint-disable-next-line +describe('dummy', () => it('should pass', () => require('chai').expect(true).to.equal(true))); + +/* +const _ = require('lodash'); +const path = require('path'); +const chai = require('chai'); +const AwsProvider = require('../../../provider/awsProvider'); +const AwsCompileFunctions = require('./index'); +const testUtils = require('../../../../../../tests/utils'); +const Serverless = require('../../../../../Serverless'); + +chai.use(require('chai-as-promised')); + +const expect = chai.expect; + +describe('AwsCompileFunctions', () => { + let serverless; + let awsCompileFunctions; + const functionName = 'test'; + const compiledFunctionName = 'TestLambdaFunction'; + + beforeEach(() => { + const options = { + stage: 'dev', + region: 'us-east-1', + }; + serverless = new Serverless(options); + serverless.setProvider('aws', new AwsProvider(serverless, options)); + serverless.cli = new serverless.classes.CLI(); + awsCompileFunctions = new AwsCompileFunctions(serverless, options); + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate = { + Resources: {}, + Outputs: {}, + }; + + const serviceArtifact = 'new-service.zip'; + const individualArtifact = 'test.zip'; + awsCompileFunctions.packagePath = testUtils.getTmpDirPath(); + // The contents of the test artifacts need to be predictable so the hashes stay the same + serverless.utils.writeFileSync(path.join(awsCompileFunctions.packagePath, + serviceArtifact), 'foobar'); + serverless.utils.writeFileSync(path.join(awsCompileFunctions.packagePath, + individualArtifact), 'barbaz'); + + awsCompileFunctions.serverless.service.service = 'new-service'; + awsCompileFunctions.serverless.service.package.artifactDirectoryName = 'somedir'; + awsCompileFunctions.serverless.service.package + .artifact = path.join(awsCompileFunctions.packagePath, serviceArtifact); + awsCompileFunctions.serverless.service.functions = {}; + awsCompileFunctions.serverless.service.functions[functionName] = { + name: 'test', + package: { + artifact: path.join(awsCompileFunctions.packagePath, + individualArtifact), + }, + handler: 'handler.hello', + }; + }); + + describe('#constructor()', () => { + it('should set the provider variable to an instance of AwsProvider', () => + expect(awsCompileFunctions.provider).to.be.instanceof(AwsProvider)); + }); + + describe('#isArnRefOrImportValue()', () => { + it('should accept a Ref', () => + expect(awsCompileFunctions.isArnRefOrImportValue({ Ref: 'DLQ' })).to.equal(true)); + it('should accept a Fn::ImportValue', () => + expect(awsCompileFunctions.isArnRefOrImportValue({ 'Fn::ImportValue': 'DLQ' })) + .to.equal(true)); + it('should reject other objects', () => + expect(awsCompileFunctions.isArnRefOrImportValue({ Blah: 'vtha' })).to.equal(false)); + }); + + describe('#compileFunctions()', () => { + it('should use service artifact if not individually', () => { + awsCompileFunctions.serverless.service.package.individually = false; + const artifactTemp = awsCompileFunctions.serverless.service.functions.test.package.artifact; + awsCompileFunctions.serverless.service.functions.test.package.artifact = false; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + const functionResource = awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate.Resources[compiledFunctionName]; + + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + + expect(functionResource.Properties.Code.S3Key) + .to.deep.equal(`${s3Folder}/${s3FileName}`); + awsCompileFunctions.serverless.service.functions.test.package.artifact = artifactTemp; + }); + }); + + it('should use function artifact if individually', () => { + awsCompileFunctions.serverless.service.package.individually = true; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + const functionResource = awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate.Resources[compiledFunctionName]; + + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service + .functions[functionName].package.artifact.split(path.sep).pop(); + + expect(functionResource.Properties.Code.S3Key) + .to.deep.equal(`${s3Folder}/${s3FileName}`); + }); + }); + + it('should use function artifact if individually at function level', () => { + awsCompileFunctions.serverless.service.functions[functionName].package.individually = true; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + const functionResource = awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate.Resources[compiledFunctionName]; + + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service + .functions[functionName].package.artifact.split(path.sep).pop(); + + expect(functionResource.Properties.Code.S3Key) + .to.deep.equal(`${s3Folder}/${s3FileName}`); + awsCompileFunctions.serverless.service.functions[functionName].package = { + individually: false, + }; + }); + }); + + it('should add an ARN provider role', () => { + awsCompileFunctions.serverless.service.provider.name = 'aws'; + awsCompileFunctions.serverless.service.provider.role = 'arn:aws:xxx:*:*'; + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.DependsOn).to.deep.equal(['FuncLogGroup']); + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.Properties.Role + ).to.deep.equal(awsCompileFunctions.serverless.service.provider.role); + }); + }); + + it('should add a logical role name provider role', () => { + awsCompileFunctions.serverless.service.provider.name = 'aws'; + awsCompileFunctions.serverless.service.provider.role = 'LogicalNameRole'; + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.DependsOn + ).to.deep.equal(['FuncLogGroup', 'LogicalNameRole']); + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.Properties.Role + ).to.deep.equal({ + 'Fn::GetAtt': [ + awsCompileFunctions.serverless.service.provider.role, + 'Arn', + ], + }); + }); + }); + + it('should add a "Fn::GetAtt" Object provider role', () => { + awsCompileFunctions.serverless.service.provider.name = 'aws'; + awsCompileFunctions.serverless.service.provider.role = { + 'Fn::GetAtt': [ + 'LogicalRoleName', + 'Arn', + ], + }; + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.DependsOn + ).to.deep.equal(['FuncLogGroup', 'LogicalRoleName']); + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.Properties.Role + ).to.deep.equal(awsCompileFunctions.serverless.service.provider.role); + }); + }); + + it('should add an ARN function role', () => { + awsCompileFunctions.serverless.service.provider.name = 'aws'; + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + role: 'arn:aws:xxx:*:*', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.DependsOn).to.deep.equal(['FuncLogGroup']); + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.Properties.Role + ).to.deep.equal(awsCompileFunctions.serverless.service.functions.func.role); + }); + }); + + it('should add a logical role name function role', () => { + awsCompileFunctions.serverless.service.provider.name = 'aws'; + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + role: 'LogicalRoleName', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.DependsOn + ).to.deep.equal(['FuncLogGroup', 'LogicalRoleName']); + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.Properties.Role + ).to.deep.equal({ + 'Fn::GetAtt': [ + awsCompileFunctions.serverless.service.functions.func.role, + 'Arn', + ], + }); + }); + }); + + it('should add a "Fn::GetAtt" Object function role', () => { + awsCompileFunctions.serverless.service.provider.name = 'aws'; + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + role: { + 'Fn::GetAtt': [ + 'LogicalRoleName', + 'Arn', + ], + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.DependsOn + ).to.deep.equal(['FuncLogGroup', 'LogicalRoleName']); + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.Properties.Role + ).to.deep.equal(awsCompileFunctions.serverless.service.functions.func.role); + }); + }); + + it('should add a "Fn::ImportValue" Object function role', () => { + awsCompileFunctions.serverless.service.provider.name = 'aws'; + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + role: { + 'Fn::ImportValue': 'Foo', + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.DependsOn).to.deep.equal(['FuncLogGroup']); + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.Properties.Role + ).to.deep.equal(awsCompileFunctions.serverless.service.functions.func.role); + }); + }); + + it('should prefer function declared role over provider declared role', () => { + awsCompileFunctions.serverless.service.provider.name = 'aws'; + awsCompileFunctions.serverless.service.provider.role = 'arn:aws:xxx:*:*'; + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + role: 'arn:aws:xxx:*:*', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.DependsOn).to.deep.equal(['FuncLogGroup']); + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.Properties.Role + ).to.equal(awsCompileFunctions.serverless.service.functions.func.role); + }); + }); + + it('should add function declared roles', () => { + awsCompileFunctions.serverless.service.provider.name = 'aws'; + awsCompileFunctions.serverless.service.functions = { + func0: { + handler: 'func.function.handler', + name: 'new-service-dev-func0', + role: 'arn:aws:xx0:*:*', + }, + func1: { + handler: 'func.function.handler', + name: 'new-service-dev-func1', + role: 'arn:aws:xx1:*:*', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.Func0LambdaFunction.DependsOn).to.deep.equal(['Func0LogGroup']); + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.Func0LambdaFunction.Properties.Role + ).to.deep.equal(awsCompileFunctions.serverless.service.functions.func0.role); + + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.Func1LambdaFunction.DependsOn).to.deep.equal(['Func1LogGroup']); + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.Func1LambdaFunction.Properties.Role + ).to.deep.equal(awsCompileFunctions.serverless.service.functions.func1.role); + }); + }); + + it('should add function declared role and fill in with provider role', () => { + awsCompileFunctions.serverless.service.provider.name = 'aws'; + awsCompileFunctions.serverless.service.provider.role = 'arn:aws:xxx:*:*'; + awsCompileFunctions.serverless.service.functions = { + func0: { + handler: 'func.function.handler', + name: 'new-service-dev-func0', + }, + func1: { + handler: 'func.function.handler', + name: 'new-service-dev-func1', + role: 'arn:aws:xx1:*:*', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.Func0LambdaFunction.DependsOn).to.deep.equal(['Func0LogGroup']); + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.Func0LambdaFunction.Properties.Role + ).to.deep.equal(awsCompileFunctions.serverless.service.provider.role); + + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.Func1LambdaFunction.DependsOn).to.deep.equal(['Func1LogGroup']); + expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.Func1LambdaFunction.Properties.Role + ).to.deep.equal(awsCompileFunctions.serverless.service.functions.func1.role); + }); + }); + + it('should reject if the function handler is not present', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + name: 'new-service-dev-func', + }, + }; + + expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); + }); + + it('should create a simple function resource', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + }, + }; + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should create a function resource with provider level vpc config', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.provider.vpc = { + securityGroupIds: ['xxx'], + subnetIds: ['xxx'], + }; + + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + }, + }; + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + VpcConfig: { + SecurityGroupIds: ['xxx'], + SubnetIds: ['xxx'], + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should create a function resource with function level vpc config', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + vpc: { + securityGroupIds: ['xxx'], + subnetIds: ['xxx'], + }, + }, + }; + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + VpcConfig: { + SecurityGroupIds: ['xxx'], + SubnetIds: ['xxx'], + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should create a function resource with provider level tags', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + }, + }; + + awsCompileFunctions.serverless.service.provider.tags = { + foo: 'bar', + baz: 'qux', + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + Tags: [ + { Key: 'foo', Value: 'bar' }, + { Key: 'baz', Value: 'qux' }, + ], + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should create a function resource with function level tags', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + tags: { + foo: 'bar', + baz: 'qux', + }, + }, + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + Tags: [ + { Key: 'foo', Value: 'bar' }, + { Key: 'baz', Value: 'qux' }, + ], + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should create a function resource with provider and function level tags', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + tags: { + foo: 'bar', + baz: 'qux', + }, + }, + }; + + awsCompileFunctions.serverless.service.provider.tags = { + foo: 'quux', + corge: 'uier', + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + Tags: [ + { Key: 'foo', Value: 'bar' }, + { Key: 'corge', Value: 'uier' }, + { Key: 'baz', Value: 'qux' }, + ], + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + describe('when using onError config', () => { + let s3Folder; + let s3FileName; + + beforeEach(() => { + s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + }); + + it('should reject if config is provided as a number', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + onError: 12, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); + }); + + it('should reject if config is provided as an object', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + onError: { + foo: 'bar', + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); + }); + + it('should reject if config is not a SNS or SQS arn', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + onError: 'foo', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); + }); + + describe('when IamRoleLambdaExecution is used', () => { + beforeEach(() => { + // pretend that the IamRoleLambdaExecution is used + awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = { + Properties: { + Policies: [ + { + PolicyDocument: { + Statement: [], + }, + }, + ], + }, + }; + }); + + it('should create necessary resources if a SNS arn is provided', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + onError: 'arn:aws:sns:region:accountid:foo', + }, + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + DeadLetterConfig: { + TargetArn: 'arn:aws:sns:region:accountid:foo', + }, + }, + }; + + const compiledDlqStatement = { + Effect: 'Allow', + Action: [ + 'sns:Publish', + ], + Resource: ['arn:aws:sns:region:accountid:foo'], + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + const compiledCfTemplate = awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate; + + const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; + const dlqStatement = compiledCfTemplate.Resources + .IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement[0]; + + expect(functionResource).to.deep.equal(compiledFunction); + expect(dlqStatement).to.deep.equal(compiledDlqStatement); + }); + }); + + it('should throw an informative error message if a SQS arn is provided', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + onError: 'arn:aws:sqs:region:accountid:foo', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()) + .to.be.rejectedWith('only supports SNS'); + }); + + it('should create necessary resources if a Ref is provided', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + onError: { + Ref: 'DLQ', + }, + }, + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + DeadLetterConfig: { + TargetArn: { + Ref: 'DLQ', + }, + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + const compiledCfTemplate = awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate; + + const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; + expect(functionResource).to.deep.equal(compiledFunction); + }); + }); + + it('should create necessary resources if a Fn::ImportValue is provided', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + onError: { + 'Fn::ImportValue': 'DLQ', + }, + }, + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + DeadLetterConfig: { + TargetArn: { + 'Fn::ImportValue': 'DLQ', + }, + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + const compiledCfTemplate = awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate; + + const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; + expect(functionResource).to.deep.equal(compiledFunction); + }); + }); + }); + + describe('when IamRoleLambdaExecution is not used', () => { + it('should create necessary function resources if a SNS arn is provided', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + onError: 'arn:aws:sns:region:accountid:foo', + }, + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + DeadLetterConfig: { + TargetArn: 'arn:aws:sns:region:accountid:foo', + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + const compiledCfTemplate = awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate; + + const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; + + expect(functionResource).to.deep.equal(compiledFunction); + }); + }); + + it('should reject with an informative error message if a SQS arn is provided', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + onError: 'arn:aws:sqs:region:accountid:foo', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()) + .to.be.rejectedWith('only supports SNS'); + }); + }); + }); + + describe('when using awsKmsKeyArn config', () => { + let s3Folder; + let s3FileName; + + beforeEach(() => { + s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + }); + + it('should reject if config is provided as a number', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + awsKmsKeyArn: 12, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()) + .to.be.rejectedWith('provided as a string'); + }); + + it('should reject if config is provided as an object', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + awsKmsKeyArn: { + foo: 'bar', + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()) + .to.be.rejectedWith('provided as a string'); + }); + + it('should throw an error if config is not a KMS key arn', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + awsKmsKeyArn: 'foo', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()) + .to.be.rejectedWith('KMS key arn'); + }); + + it('should use a the service KMS key arn if provided', () => { + awsCompileFunctions.serverless.service.serviceObject = { + name: 'new-service', + awsKmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar', + }; + + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + }, + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + KmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + const compiledCfTemplate = awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate; + const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; + expect(functionResource).to.deep.equal(compiledFunction); + }); + }); + + it('should prefer a function KMS key arn over a service KMS key arn', () => { + awsCompileFunctions.serverless.service.serviceObject = { + name: 'new-service', + awsKmsKeyArn: 'arn:aws:kms:region:accountid:foo/service', + }; + + awsCompileFunctions.serverless.service.functions = { + func1: { + handler: 'func1.function.handler', + name: 'new-service-dev-func1', + awsKmsKeyArn: 'arn:aws:kms:region:accountid:foo/function', + }, + func2: { + handler: 'func2.function.handler', + name: 'new-service-dev-func2', + }, + }; + + const compiledFunction1 = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'Func1LogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func1', + Handler: 'func1.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + KmsKeyArn: 'arn:aws:kms:region:accountid:foo/function', + }, + }; + + const compiledFunction2 = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'Func2LogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func2', + Handler: 'func2.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + KmsKeyArn: 'arn:aws:kms:region:accountid:foo/service', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + const compiledCfTemplate = awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate; + + const function1Resource = compiledCfTemplate.Resources.Func1LambdaFunction; + const function2Resource = compiledCfTemplate.Resources.Func2LambdaFunction; + expect(function1Resource).to.deep.equal(compiledFunction1); + expect(function2Resource).to.deep.equal(compiledFunction2); + }); + }); + + describe('when IamRoleLambdaExecution is used', () => { + beforeEach(() => { + // pretend that the IamRoleLambdaExecution is used + awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = { + Properties: { + Policies: [ + { + PolicyDocument: { + Statement: [], + }, + }, + ], + }, + }; + }); + + it('should create necessary resources if a KMS key arn is provided', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + awsKmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar', + }, + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + KmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar', + }, + }; + + const compiledKmsStatement = { + Effect: 'Allow', + Action: [ + 'kms:Decrypt', + ], + Resource: ['arn:aws:kms:region:accountid:foo/bar'], + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + const compiledCfTemplate = awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate; + + const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; + const dlqStatement = compiledCfTemplate.Resources + .IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement[0]; + + expect(functionResource).to.deep.equal(compiledFunction); + expect(dlqStatement).to.deep.equal(compiledKmsStatement); + }); + }); + }); + + describe('when IamRoleLambdaExecution is not used', () => { + it('should create necessary function resources if a KMS key arn is provided', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + awsKmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar', + }, + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + KmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + const compiledCfTemplate = awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate; + + const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction; + + expect(functionResource).to.deep.equal(compiledFunction); + }); + }); + }); + }); + + it('should create a function resource with environment config', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + environment: { + test1: 'test1', + test2: 'test2', + }, + }, + }; + + awsCompileFunctions.serverless.service.provider.environment = { + providerTest1: 'providerTest1', + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + Environment: { + Variables: { + test1: 'test1', + test2: 'test2', + providerTest1: 'providerTest1', + }, + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should create a function resource with function level environment config', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + environment: { + test1: 'test1', + }, + }, + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + Environment: { + Variables: { + test1: 'test1', + }, + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should create a function resource with provider level environment config', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + }, + }; + + awsCompileFunctions.serverless.service.provider.environment = { + providerTest1: 'providerTest1', + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + Environment: { + Variables: { + providerTest1: 'providerTest1', + }, + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should overwrite a provider level environment config when function config is given', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.provider.environment = { + variable: 'overwrite-me', + }; + + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + environment: { + variable: 'overwritten', + }, + }, + }; + + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + Environment: { + Variables: { + variable: 'overwritten', + }, + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should throw an error if environment variable has invalid name', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + environment: { + '1test1': 'test1', + test2: 'test2', + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); + }); + + it('should accept an environment variable with a not-string value', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + environment: { + counter: 18, + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect(awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.Properties.Environment.Variables.counter + ).to.equal(18); + }); + }); + + it('should accept an environment variable with CF ref and functions', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + environment: { + counter: { + Ref: 'TestVariable', + }, + list: { + 'Fn::Join:': [', ', ['a', 'b', 'c']], + }, + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + environment: { + counter: { + NotRef: 'TestVariable', + }, + }, + }, + }; + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); + }); + }); + + it('should consider function based config when creating a function resource', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.functions = { + func: { + name: 'customized-func-function', + handler: 'func.function.handler', + memorySize: 128, + timeout: 10, + }, + }; + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'customized-func-function', + Handler: 'func.function.handler', + MemorySize: 128, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 10, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should allow functions to use a different runtime' + + ' than the service default runtime if specified', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + runtime: 'python2.7', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'python2.7', + Timeout: 6, + }, + }; + + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should default to the nodejs4.3 runtime when no provider runtime is given', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.provider.runtime = null; + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + }, + }; + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should consider the providers runtime and memorySize ' + + 'when creating a function resource', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.provider.runtime = 'python2.7'; + awsCompileFunctions.serverless.service.provider.memorySize = 128; + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + }, + }; + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 128, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'python2.7', + Timeout: 6, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should use a custom bucket if specified', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + const bucketName = 'com.serverless.deploys'; + + awsCompileFunctions.serverless.service.package.deploymentBucket = bucketName; + awsCompileFunctions.serverless.service.provider.runtime = 'python2.7'; + awsCompileFunctions.serverless.service.provider.memorySize = 128; + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + }, + }; + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: bucketName, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 128, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'python2.7', + Timeout: 6, + }, + }; + const coreCloudFormationTemplate = awsCompileFunctions.serverless.utils.readFileSync( + path.join( + __dirname, + '..', + '..', + 'lib', + 'core-cloudformation-template.json' + ) + ); + awsCompileFunctions.serverless.service.provider + .compiledCloudFormationTemplate = coreCloudFormationTemplate; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should include description if specified', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + description: 'Lambda function description', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction.Properties.Description + ).to.equal('Lambda function description'); + }); + }); + + it('should create corresponding function output and version objects', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + }, + anotherFunc: { + handler: 'anotherFunc.function.handler', + }, + }; + + const expectedOutputs = { + FuncLambdaFunctionQualifiedArn: { + Description: 'Current Lambda function version', + Value: { Ref: 'FuncLambdaVersionl6Rjpaz0gycgsEDI51sLed039fH2uR4W8Q2IW8cNo' }, + }, + AnotherFuncLambdaFunctionQualifiedArn: { + Description: 'Current Lambda function version', + Value: { + Ref: 'AnotherFuncLambdaVersion6JZQneYqP4bC0Z3ywMc3XJPyECHK4RMGhpv8iis4E', + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Outputs + ).to.deep.equal( + expectedOutputs + ); + }); + }); + + it('should create a new version object if only the configuration changed', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + }, + anotherFunc: { + handler: 'anotherFunc.function.handler', + }, + }; + + const expectedOutputs = { + FuncLambdaFunctionQualifiedArn: { + Description: 'Current Lambda function version', + Value: { Ref: 'FuncLambdaVersionl6Rjpaz0gycgsEDI51sLed039fH2uR4W8Q2IW8cNo' }, + }, + AnotherFuncLambdaFunctionQualifiedArn: { + Description: 'Current Lambda function version', + Value: { + Ref: 'AnotherFuncLambdaVersion6JZQneYqP4bC0Z3ywMc3XJPyECHK4RMGhpv8iis4E', + }, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Outputs + ).to.deep.equal( + expectedOutputs + ); + + // Change configuration + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate = { + Resources: {}, + Outputs: {}, + }; + + _.set( + awsCompileFunctions, + 'serverless.service.functions.func.environment.MY_ENV_VAR', + 'myvalue' + ); + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled; + }) + .then(() => { + // Expect different version hash + _.set( + expectedOutputs, + 'FuncLambdaFunctionQualifiedArn', + { + Description: 'Current Lambda function version', + Value: { Ref: 'FuncLambdaVersiona6VymfU25aF6eS2qysm7sHqPyy8RqYUzoTvDeBrrBA' }, + } + ); + + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Outputs + ).to.deep.equal( + expectedOutputs + ); + }); + }); + + it('should include description under version too if function is specified', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + description: 'Lambda function description', + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaVersionOKy3yjVllZnozzdvQqHlRN8lBwkZyA6l76TCAEyork + .Properties.Description + ).to.equal('Lambda function description'); + }); + }); + + it('should not create function output objects when "versionFunctions" is false', () => { + awsCompileFunctions.serverless.service.provider.versionFunctions = false; + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + }, + anotherFunc: { + handler: 'anotherFunc.function.handler', + }, + }; + + const expectedOutputs = {}; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Outputs + ).to.deep.equal( + expectedOutputs + ); + }); + }); + + it('should set function declared reserved concurrency limit', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + reservedConcurrency: 5, + }, + }; + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + ReservedConcurrentExecutions: 5, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should throw an informative error message if non-integer reserved concurrency limit set ' + + 'on function', () => { + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + reservedConcurrency: '1', + }, + }; + + const errorMessage = [ + 'You should use integer as reservedConcurrency value on function: ', + 'new-service-dev-func', + ].join(''); + + return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(errorMessage); + }); + }); + + describe('#compileRole()', () => { + it('adds the default role with DependsOn values', () => { + const role = 'IamRoleLambdaExecution'; + const resource = { Properties: {} }; + awsCompileFunctions.compileRole(resource, role); + + expect(resource).to.deep.equal({ + DependsOn: [ + role, + ], + Properties: { + Role: { + 'Fn::GetAtt': [ + role, + 'Arn', + ], + }, + }, + }); + }); + + it('adds a role based on a logical name with DependsOn values', () => { + const role = 'LogicalRoleName'; + const resource = { Properties: {} }; + awsCompileFunctions.compileRole(resource, role); + + expect(resource).to.deep.equal({ + DependsOn: [ + role, + ], + Properties: { + Role: { + 'Fn::GetAtt': [ + role, + 'Arn', + ], + }, + }, + }); + }); + + it('adds a role based on a Fn::GetAtt with DependsOn values', () => { + const role = { 'Fn::GetAtt': ['Foo', 'Arn'] }; + const resource = { Properties: {} }; + awsCompileFunctions.compileRole(resource, role); + + expect(resource).to.deep.equal({ + DependsOn: [ + 'Foo', + ], + Properties: { + Role: role, + }, + }); + }); + + it('adds a role based on a Fn::ImportValue', () => { + const role = { 'Fn::ImportValue': 'Foo' }; + const resource = { Properties: {} }; + awsCompileFunctions.compileRole(resource, role); + + expect(resource).to.deep.equal({ + Properties: { + Role: role, + }, + }); + }); + + it('adds a role based on a predefined arn string', () => { + const role = 'arn:aws:xxx:*:*'; + const resource = { Properties: {} }; + awsCompileFunctions.compileRole(resource, role); + + expect(resource).to.deep.equal({ + Properties: { + Role: role, + }, + }); + }); + + describe('Errors if unsupported object type is provided', () => { + it('should throw for object type { Ref: "Foo" }', () => { + const role = { Ref: 'Foo' }; + const resource = { Properties: {} }; + + expect(() => + awsCompileFunctions.compileRole(resource, role) + ).to.throw(Error); + }); + + it('should throw for object type Buffer', () => { + const role = new Buffer('Foo'); + const resource = { Properties: {} }; + + expect(() => + awsCompileFunctions.compileRole(resource, role) + ).to.throw(Error); + }); + + it('should throw for object type Array', () => { + const role = [1, 2, 3]; + const resource = { Properties: {} }; + + expect(() => + awsCompileFunctions.compileRole(resource, role) + ).to.throw(Error); + }); + }); + + it('should not set unset properties when not specified in yml (layers, vpc, etc)', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + }, + }; + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + + it('should set Layers when specified', () => { + const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; + const s3FileName = awsCompileFunctions.serverless.service.package.artifact + .split(path.sep).pop(); + + awsCompileFunctions.serverless.service.functions = { + func: { + handler: 'func.function.handler', + name: 'new-service-dev-func', + layers: ['arn:aws:xxx:*:*'], + }, + }; + const compiledFunction = { + Type: 'AWS::Lambda::Function', + DependsOn: [ + 'FuncLogGroup', + 'IamRoleLambdaExecution', + ], + Properties: { + Code: { + S3Key: `${s3Folder}/${s3FileName}`, + S3Bucket: { Ref: 'ServerlessDeploymentBucket' }, + }, + FunctionName: 'new-service-dev-func', + Handler: 'func.function.handler', + MemorySize: 1024, + Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] }, + Runtime: 'nodejs4.3', + Timeout: 6, + Layers: ['arn:aws:xxx:*:*'], + }, + }; + + return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled + .then(() => { + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate + .Resources.FuncLambdaFunction + ).to.deep.equal(compiledFunction); + }); + }); + }); +}); +*/ diff --git a/lib/plugins/aws/package/index.js b/lib/plugins/aws/package/index.js index 5b9e23b67..b461fdb2c 100644 --- a/lib/plugins/aws/package/index.js +++ b/lib/plugins/aws/package/index.js @@ -65,6 +65,9 @@ class AwsPackage { 'before:package:compileFunctions': () => BbPromise.bind(this) .then(this.generateArtifactDirectoryName), + 'before:package:compileLayers': () => BbPromise.bind(this) + .then(this.generateArtifactDirectoryName), + 'package:finalize': () => BbPromise.bind(this) .then(() => this.serverless.pluginManager.spawn('aws:package:finalize')), diff --git a/lib/plugins/aws/package/lib/generateArtifactDirectoryName.js b/lib/plugins/aws/package/lib/generateArtifactDirectoryName.js index 795fd3081..c0d1f6b33 100644 --- a/lib/plugins/aws/package/lib/generateArtifactDirectoryName.js +++ b/lib/plugins/aws/package/lib/generateArtifactDirectoryName.js @@ -4,12 +4,15 @@ const BbPromise = require('bluebird'); module.exports = { generateArtifactDirectoryName() { - const date = new Date(); - const serviceStage = `${this.serverless.service.service}/${this.provider.getStage()}`; - const dateString = `${date.getTime().toString()}-${date.toISOString()}`; - const prefix = this.provider.getDeploymentPrefix(); - this.serverless.service.package - .artifactDirectoryName = `${prefix}/${serviceStage}/${dateString}`; + // Don't regenerate name if it's already set + if (!this.serverless.service.package.artifactDirectoryName) { + const date = new Date(); + const serviceStage = `${this.serverless.service.service}/${this.provider.getStage()}`; + const dateString = `${date.getTime().toString()}-${date.toISOString()}`; + const prefix = this.provider.getDeploymentPrefix(); + this.serverless.service.package + .artifactDirectoryName = `${prefix}/${serviceStage}/${dateString}`; + } return BbPromise.resolve(); }, diff --git a/lib/plugins/package/lib/packageService.js b/lib/plugins/package/lib/packageService.js index 8773619d9..0ff1b4bb7 100644 --- a/lib/plugins/package/lib/packageService.js +++ b/lib/plugins/package/lib/packageService.js @@ -24,15 +24,18 @@ module.exports = { return _.union(packageIncludes, include); }, - getExcludes(exclude) { + getExcludes(exclude, excludeLayers) { const packageExcludes = this.serverless.service.package.exclude || []; // add local service plugins Path const pluginsLocalPath = this.serverless.pluginManager .parsePluginsObject(this.serverless.service.plugins).localPath; - const localPathExcludes = pluginsLocalPath ? [pluginsLocalPath] : []; + // add layer paths + const layerExcludes = excludeLayers ? this.serverless.service.getAllLayers().map( + (layer) => `${this.serverless.service.getLayer(layer).path}/**`) : []; // add defaults for exclude - return _.union(this.defaultExcludes, localPathExcludes, packageExcludes, exclude); + return _.union( + this.defaultExcludes, localPathExcludes, packageExcludes, layerExcludes, exclude); }, packageService() { @@ -56,6 +59,15 @@ module.exports = { shouldPackageService = true; return BbPromise.resolve(); }); + const allLayers = this.serverless.service.getAllLayers(); + packagePromises.push(..._.map(allLayers, layerName => { + const layerObject = this.serverless.service.getLayer(layerName); + layerObject.package = layerObject.package || {}; + if (layerObject.package.artifact) { + return BbPromise.resolve(); + } + return this.packageLayer(layerName); + })); return BbPromise.all(packagePromises).then(() => { if (shouldPackageService && !this.serverless.service.package.artifact) { @@ -113,8 +125,24 @@ module.exports = { ); }, + packageLayer(layerName) { + const layerObject = this.serverless.service.getLayer(layerName); + + const zipFileName = `${layerName}.zip`; + + return this.resolveFilePathsLayer(layerName) + .then(filePaths => + this.zipFiles(filePaths, zipFileName, layerObject.path).then(artifactPath => { + layerObject.package = { + artifact: artifactPath, + }; + return artifactPath; + }) + ); + }, + resolveFilePathsAll() { - const params = { exclude: this.getExcludes(), include: this.getIncludes() }; + const params = { exclude: this.getExcludes([], true), include: this.getIncludes() }; return this.excludeDevDependencies(params).then(() => this.resolveFilePathsFromPatterns(params)); }, @@ -124,14 +152,26 @@ module.exports = { const funcPackageConfig = functionObject.package || {}; const params = { - exclude: this.getExcludes(funcPackageConfig.exclude), + exclude: this.getExcludes(funcPackageConfig.exclude, true), include: this.getIncludes(funcPackageConfig.include), }; return this.excludeDevDependencies(params).then(() => this.resolveFilePathsFromPatterns(params)); }, - resolveFilePathsFromPatterns(params) { + resolveFilePathsLayer(layerName) { + const layerObject = this.serverless.service.getLayer(layerName); + const layerPackageConfig = layerObject.package || {}; + + const params = { + exclude: this.getExcludes(layerPackageConfig.exclude), + include: this.getIncludes(layerPackageConfig.include), + }; + return this.excludeDevDependencies(params).then(() => this.resolveFilePathsFromPatterns( + params, layerObject.path)); + }, + + resolveFilePathsFromPatterns(params, prefix) { const patterns = ['**']; params.exclude.forEach((pattern) => { @@ -149,7 +189,7 @@ module.exports = { }); return globby(patterns, { - cwd: this.serverless.config.servicePath, + cwd: path.join(this.serverless.config.servicePath, prefix || ''), dot: true, silent: true, follow: true, diff --git a/lib/plugins/package/lib/zipService.js b/lib/plugins/package/lib/zipService.js index 4c17a1b6f..eaba9869d 100644 --- a/lib/plugins/package/lib/zipService.js +++ b/lib/plugins/package/lib/zipService.js @@ -55,7 +55,7 @@ module.exports = { this.zipFiles(filePaths, params.zipFileName)); }, - zipFiles(files, zipFileName) { + zipFiles(files, zipFileName, prefix) { if (files.length === 0) { const error = new this.serverless.classes.Error('No files to package'); return BbPromise.reject(error); @@ -80,10 +80,12 @@ module.exports = { output.on('open', () => { zip.pipe(output); - BbPromise.all(files.map(this.getFileContentAndStat.bind(this))).then((contents) => { + const filePaths = files.map(file => (prefix ? path.join(prefix, file) : file)); + BbPromise.all(filePaths.map(this.getFileContentAndStat.bind(this))).then((contents) => { _.forEach(_.sortBy(contents, ['filePath']), (file) => { + const name = file.filePath.slice(prefix ? `${prefix}${path.sep}`.length : 0); zip.append(file.data, { - name: file.filePath, + name, mode: file.stat.mode, date: new Date(0), // necessary to get the same hash when zipping the same content }); diff --git a/lib/plugins/package/package.js b/lib/plugins/package/package.js index 80f5f8055..7e8f89ad7 100644 --- a/lib/plugins/package/package.js +++ b/lib/plugins/package/package.js @@ -29,6 +29,7 @@ class Package { 'initialize', 'setupProviderConfiguration', 'createDeploymentArtifacts', + 'compileLayers', 'compileFunctions', 'compileEvents', 'finalize',