diff --git a/.gitignore b/.gitignore index 7967499..9a73efe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ /node_modules -/coverage -/.nyc_output # Jetbrains IDEs .idea/ diff --git a/lib/stackops/apiGateway.js b/lib/stackops/apiGateway.js index 196c062..96bd9c6 100644 --- a/lib/stackops/apiGateway.js +++ b/lib/stackops/apiGateway.js @@ -186,7 +186,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac this.options.verbose && this._serverless.cli.log('Configuring stage'); const stageResource = internal.createStageResource.call(this, `${stackName}-ApiGatewayRestApi`, deploymentName); aliasResources.push({ ApiGatewayStage: stageResource }); - + const baseMapping = _.assign({}, _.pickBy(stageStack.Resources, ['Type', 'AWS::ApiGateway::BasePathMapping'])); if (!_.isEmpty(baseMapping)) { const baseMappingName = _.keys(baseMapping)[0]; @@ -198,7 +198,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac aliasResources.push(baseMapping); delete stageStack.Resources[baseMappingName]; } - } + } // Fetch lambda permissions, methods and resources. These have to be updated later to allow the aliased functions. const apiLambdaPermissions = @@ -278,9 +278,9 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac // Adjust permission to reference the function aliases _.forOwn(apiLambdaPermissions, (permission, name) => { const functionName = _.replace(name, /LambdaPermissionApiGateway$/, ''); - const versionName = _.find(_.keys(versions), version => _.startsWith(version, functionName)); - const aliasName = _.find(_.keys(aliases), alias => _.startsWith(alias, functionName)); - const isExternalRef = isExternalRefAuthorizerPredicate(permission.Properties.FunctionName); + const versionName = utils.getFunctionVersionName(versions, functionName); + const aliasName = utils.getAliasVersionName(aliases, functionName); + const isExternalRef = isExternalRefAuthorizerPredicate(permission.Properties.FunctionName); // Adjust references and alias permissions if (!isExternalRef) { @@ -305,9 +305,9 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac ] }; } - + // Add dependency on function version - if (!isExternalRef) { + if (!isExternalRef) { permission.DependsOn = [ versionName, aliasName ]; } else { permission.DependsOn = _.compact([ versionName, aliasName ]); diff --git a/lib/stackops/snsEvents.js b/lib/stackops/snsEvents.js index 5f40113..01767a9 100644 --- a/lib/stackops/snsEvents.js +++ b/lib/stackops/snsEvents.js @@ -31,8 +31,8 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac _.forOwn(lambdaSubscriptions, subscription => { const functionNameRef = utils.findAllReferences(_.get(subscription, 'Endpoint')); const functionName = _.replace(_.get(functionNameRef, '[0].ref', ''), /LambdaFunction$/, ''); - const versionName = _.find(_.keys(versions), version => _.startsWith(version, functionName)); - const aliasName = _.find(_.keys(aliases), alias => _.startsWith(alias, functionName)); + const versionName = utils.getFunctionVersionName(versions, functionName); + const aliasName = utils.getAliasVersionName(aliases, functionName); subscription.Endpoint = { Ref: aliasName }; @@ -54,8 +54,8 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac const functionNameRef = utils.findAllReferences(_.get(subscription.Properties, 'Endpoint')); const functionName = _.replace(_.get(functionNameRef, '[0].ref', ''), /LambdaFunction$/, ''); - const versionName = _.find(_.keys(versions), version => _.startsWith(version, functionName)); - const aliasName = _.find(_.keys(aliases), alias => _.startsWith(alias, functionName)); + const versionName = utils.getFunctionVersionName(versions, functionName); + const aliasName = utils.getAliasVersionName(aliases, functionName); subscription.Properties.Endpoint = { Ref: aliasName }; subscription.DependsOn = [ versionName, aliasName ]; @@ -72,8 +72,8 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac // Adjust permission to reference the function aliases _.forOwn(snsLambdaPermissions, (permission, name) => { const functionName = _.replace(name, /LambdaPermission.*$/, ''); - const versionName = _.find(_.keys(versions), version => _.startsWith(version, functionName)); - const aliasName = _.find(_.keys(aliases), alias => _.startsWith(alias, functionName)); + const versionName = utils.getFunctionVersionName(versions, functionName); + const aliasName = utils.getAliasVersionName(aliases, functionName); // Adjust references and alias permissions permission.Properties.FunctionName = { Ref: aliasName }; diff --git a/lib/utils.js b/lib/utils.js index 824b8aa..fcc8d53 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -89,7 +89,8 @@ class Utils { /** * Checks if a CF resource permission targets the given service as Principal. - * @param {string} service + * @param {Object} permission + * @param {string} service */ static hasPermissionPrincipal(permission, service) { const principal = _.get(permission, 'Properties.Principal'); @@ -104,6 +105,23 @@ class Utils { return false; } + /** + * @param {object} versions + * @param {string} functionName + * @returns {string} + */ + static getFunctionVersionName(versions, functionName) { + return _.find(_.keys(versions), version => version === `${functionName}LambdaVersion`); + } + + /** + * @param {object} aliases + * @param {string} functionName + * @returns {string} + */ + static getAliasVersionName(aliases, functionName) { + return _.find(_.keys(aliases), alias => alias === `${functionName}Alias`); + } } module.exports = Utils; diff --git a/test/data/alias-stack-1.json b/test/data/alias-stack-1.json index 8dc5d0a..5f0a0a6 100644 --- a/test/data/alias-stack-1.json +++ b/test/data/alias-stack-1.json @@ -9,6 +9,25 @@ "RetentionInDays": 7 } }, + "Testfct1WithSuffixAlias": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "Description": "Echo function echoes alias", + "FunctionName": { + "Fn::ImportValue": "sls-test-project-dev-Testfct1WithSuffix-LambdaFunctionArn" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "Testfct1WithSuffixLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU", + "Version" + ] + }, + "Name": "myAlias" + }, + "DependsOn": [ + "Testfct1WithSuffixLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU" + ] + }, "Testfct1Alias": { "Type": "AWS::Lambda::Alias", "Properties": { @@ -47,6 +66,17 @@ "WarmUpPluginLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU" ] }, + "Testfct1WithSuffixLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Delete", + "Properties": { + "FunctionName": { + "Fn::ImportValue": "sls-test-project-dev-Testfct1WithSuffix-LambdaFunctionArn" + }, + "CodeSha256": "Wh5jTkiTR67+V05RPWQIlzPI25WiPbdHDYNgbtAMneU=", + "Description": "Echo function echoes alias" + } + }, "Testfct1LambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU": { "Type": "AWS::Lambda::Version", "DeletionPolicy": "Delete", @@ -97,6 +127,40 @@ "ApiGatewayDeployment1494367071211" ] }, + "Testfct1WithSuffixLambdaPermissionApiGateway": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": { + "Ref": "Testfct1WithSuffixAlias" + }, + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:aws:execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Fn::ImportValue": "sls-test-project-dev-ApiGatewayRestApi" + }, + "/*/*" + ] + ] + } + }, + "DependsOn": [ + "Testfct1WithSuffixLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU", + "Testfct1WithSuffixAlias" + ] + }, "Testfct1LambdaPermissionApiGateway": { "Type": "AWS::Lambda::Permission", "Properties": { diff --git a/test/data/sls-stack-1.json b/test/data/sls-stack-1.json index 9b243c8..8f41b38 100644 --- a/test/data/sls-stack-1.json +++ b/test/data/sls-stack-1.json @@ -11,6 +11,12 @@ "LogGroupName": "/aws/lambda/sls-test-project-dev-testfct1" } }, + "Testfct1WithSuffixLogGroup": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/lambda/sls-test-project-dev-testfct1-with-suffix" + } + }, "WarmUpPluginLogGroup": { "Type": "AWS::Logs::LogGroup", "Properties": { @@ -197,6 +203,70 @@ "Description": "Echo function echoes alias" } }, + "Testfct1WithSuffixLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/sls-test-project/dev/1494367071172-2017-05-09T21:57:51.172Z/sls-test-project.zip" + }, + "FunctionName": "sls-test-project-dev-testfct1-with-suffix", + "Handler": "handlers/testfct1-with-suffix/handler.handle", + "MemorySize": 512, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "Runtime": "nodejs4.3", + "Timeout": 15, + "Description": "Echo function echoes alias", + "Environment": { + "Variables": { + "SERVERLESS_PROJECT_NAME": "sls-test-project", + "SERVERLESS_PROJECT": "sls-test-project", + "SERVERLESS_STAGE": "dev", + "SERVERLESS_REGION": "us-east-1", + "TEST_TABLE_NAME": { + "Ref": "TestDynamoDbTable" + } + } + }, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::ImportValue": "stashimi-dev-PrivateSG" + } + ], + "SubnetIds": [ + { + "Fn::ImportValue": "stashimi-dev-PrivateSubnet1" + }, + { + "Fn::ImportValue": "stashimi-dev-PrivateSubnet2" + } + ] + } + }, + "DependsOn": [ + "Testfct1WithSuffixLogGroup", + "IamRoleLambdaExecution" + ] + }, + "Testfct1WithSuffixLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "Testfct1WithSuffixLambdaFunction" + }, + "CodeSha256": "Wh5jTkiTR67+V05RPWQIlzPI25WiPbdHDYNgbtAMneU=", + "Description": "Echo function echoes alias" + } + }, "WarmUpPluginLambdaFunction": { "Type": "AWS::Lambda::Function", "Properties": { @@ -401,6 +471,104 @@ ] } } + }, + "ApiGatewayResourceFunc2": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "ApiGatewayRestApi", + "RootResourceId" + ] + }, + "PathPart": "func1-with-suffix", + "RestApiId": { + "Ref": "ApiGatewayRestApi" + } + } + }, + "ApiGatewayMethodFunc2Get": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "RequestParameters": {}, + "ResourceId": { + "Ref": "ApiGatewayResourceFunc2" + }, + "RestApiId": { + "Ref": "ApiGatewayRestApi" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "Testfct1WithSuffixLambdaFunction", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "MethodResponses": [] + } + }, + "ApiGatewayDeployment1494367071212": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "ApiGatewayRestApi" + }, + "StageName": "dev" + }, + "DependsOn": [ + "ApiGatewayMethodFunc2Get" + ] + }, + "Testfct1WithSuffixLambdaPermissionApiGateway": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": { + "Fn::GetAtt": [ + "Testfct1WithSuffixLambdaFunction", + "Arn" + ] + }, + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:aws:execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "ApiGatewayRestApi" + }, + "/*/*" + ] + ] + } + } } }, "Outputs": { @@ -415,6 +583,12 @@ "Ref": "Testfct1LambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU" } }, + "Testfct1WithSuffixLambdaFunctionQualifiedArn": { + "Description": "Current Lambda function version", + "Value": { + "Ref": "Testfct1WithSuffixLambdaVersionWh5jTkiTR67V05RPWQIlzPI25WiPbdHDYNgbtAMneU" + } + }, "WarmUpPluginLambdaFunctionQualifiedArn": { "Description": "Current Lambda function version", "Value": { diff --git a/test/stackops/apiGateway.test.js b/test/stackops/apiGateway.test.js index 2cb3900..ed04f33 100644 --- a/test/stackops/apiGateway.test.js +++ b/test/stackops/apiGateway.test.js @@ -1,6 +1,6 @@ 'use strict'; /** - * Unit tests for SNS events. + * Unit tests for API Gateway resources. */ const { getInstalledPathSync } = require('get-installed-path'); @@ -608,8 +608,17 @@ describe('API Gateway', () => { describe('#aliasHandleApiGateway()', () => { it('should succeed with standard template', () => { serverless.service.provider.compiledCloudFormationTemplate = require('../data/sls-stack-1.json'); - serverless.service.provider.compiledCloudFormationAliasTemplate = require('../data/alias-stack-1.json'); - return expect(awsAlias.aliasHandleApiGateway({}, [], {})).to.be.fulfilled; + const compiledAliasTemplate = require('../data/alias-stack-1.json'); + serverless.service.provider.compiledCloudFormationAliasTemplate = compiledAliasTemplate; + return expect(awsAlias.aliasHandleApiGateway({}, [], {})).to.be.fulfilled + .then(() => BbPromise.all([ + expect(compiledAliasTemplate) + .to.have.a.nested.property('Resources.Testfct1LambdaPermissionApiGateway.Properties.FunctionName') + .that.deep.equals({ Ref: 'Testfct1Alias' }), + expect(compiledAliasTemplate) + .to.have.a.nested.property('Resources.Testfct1WithSuffixLambdaPermissionApiGateway.Properties.FunctionName') + .that.deep.equals({ Ref: 'Testfct1WithSuffixAlias' }), + ])); }); describe('authorizer transform', () => { @@ -697,13 +706,12 @@ describe('API Gateway', () => { .to.have.a.nested.property('Resources.TestauthLambdaPermissionApiGateway.DependsOn') .that.is.empty ])); - }); it('should support externally referenced custom authorizers with Pseudo Parameters', () => { stackTemplate = _.cloneDeep(require('../data/auth-stack-2.json')); const template = serverless.service.provider.compiledCloudFormationTemplate = stackTemplate; - const compiledAliasTemplate = serverless.service.provider.compiledCloudFormationAliasTemplate = aliasTemplate; + serverless.service.provider.compiledCloudFormationAliasTemplate = aliasTemplate; return expect(awsAlias.aliasHandleApiGateway({}, [], {})).to.be.fulfilled .then(() => BbPromise.all([ expect(template) @@ -736,7 +744,7 @@ describe('API Gateway', () => { it('should move base mappings to alias stack', () => { stackTemplate = _.cloneDeep(require('../data/auth-stack-2.json')); const template = serverless.service.provider.compiledCloudFormationTemplate = stackTemplate; - const compiledAliasTemplate = serverless.service.provider.compiledCloudFormationAliasTemplate = aliasTemplate; + serverless.service.provider.compiledCloudFormationAliasTemplate = aliasTemplate; return expect(awsAlias.aliasHandleApiGateway({}, [], {})).to.be.fulfilled .then(()=> BbPromise.all([ expect(template) @@ -753,7 +761,6 @@ describe('API Gateway', () => { } }) ])); - }); it('should transform string dependencies and references to authorizers', () => { @@ -804,7 +811,6 @@ describe('API Gateway', () => { expect(serverless).to.not.have.a.nested.property('service.resources.Resources.TestauthApiGatewayAuthorizer'), ])); }); - }); }); });