From 35f7ab80d39499ab589a24e0b010f81b8562fa9d Mon Sep 17 00:00:00 2001 From: mike-suggitt Date: Tue, 28 Jan 2020 15:48:42 +0000 Subject: [PATCH] - Upgraded eslint package - Added support for ecma 9 (inclusive of spread operator) - Updated editorconfig so all files are consistent with tab (was plying hell with linting in IDE) - Added a generic method for moving resources from stageStack into aliasStack - Moved api keys, usage plans and usage plan keys into alias - Added tests --- .editorconfig | 2 +- .eslintrc | 11 +- lib/stackops/apiGateway.js | 144 ++++++++++++++------- package-lock.json | 212 +++++++++++++++++++++++++------ package.json | 2 +- test/data/auth-stack-2.json | 106 ++++++++++++++-- test/stackops/apiGateway.test.js | 153 +++++++++++++++++++++- 7 files changed, 525 insertions(+), 105 deletions(-) diff --git a/.editorconfig b/.editorconfig index 5e54f5c..f6a1dc8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,5 +7,5 @@ end_of_line = lf charset = utf-8 [*.{json,yml}] -indent_style = space +indent_style = tab indent_size = 2 diff --git a/.eslintrc b/.eslintrc index ee3edaa..6424200 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,6 +10,9 @@ extends: - plugin:import/errors - plugin:import/warnings +parserOptions: + ecmaVersion: 9 + plugins: - promise - lodash @@ -19,8 +22,7 @@ rules: indent: - error - tab - - - MemberExpression: off + - MemberExpression: off linebreak-style: - error - unix @@ -42,6 +44,5 @@ rules: lodash/prop-shorthand: off lodash/prefer-lodash-method: - error - - - ignoreObjects: - - BbPromise + - ignoreObjects: + - BbPromise diff --git a/lib/stackops/apiGateway.js b/lib/stackops/apiGateway.js index 26e069b..45e96fe 100644 --- a/lib/stackops/apiGateway.js +++ b/lib/stackops/apiGateway.js @@ -7,18 +7,17 @@ */ const _ = require('lodash'); -const BbPromise = require('bluebird'); const utils = require('../utils'); const stageMethodConfigMappings = { - cacheDataEncrypted: { prop: 'CacheDataEncrypted', validate: _.isBoolean, default: false }, - cacheTtlInSeconds: { prop: 'CacheTtlInSeconds', validate: _.isInteger }, - cachingEnabled: { prop: 'CachingEnabled', validate: _.isBoolean, default: false }, - dataTraceEnabled: { prop: 'DataTraceEnabled', validate: _.isBoolean, default: false }, - loggingLevel: { prop: 'LoggingLevel', validate: value => _.includes([ 'OFF', 'INFO', 'ERROR' ], value), default: 'OFF' }, - metricsEnabled: { prop: 'MetricsEnabled', validate: _.isBoolean, default: false }, - throttlingBurstLimit: { prop: 'ThrottlingBurstLimit', validate: _.isInteger }, - throttlingRateLimit: { prop: 'ThrottlingRateLimit', validate: _.isNumber } + cacheDataEncrypted: {prop: 'CacheDataEncrypted', validate: _.isBoolean, default: false}, + cacheTtlInSeconds: {prop: 'CacheTtlInSeconds', validate: _.isInteger}, + cachingEnabled: {prop: 'CachingEnabled', validate: _.isBoolean, default: false}, + dataTraceEnabled: {prop: 'DataTraceEnabled', validate: _.isBoolean, default: false}, + loggingLevel: {prop: 'LoggingLevel', validate: value => _.includes(['OFF', 'INFO', 'ERROR'], value), default: 'OFF'}, + metricsEnabled: {prop: 'MetricsEnabled', validate: _.isBoolean, default: false}, + throttlingBurstLimit: {prop: 'ThrottlingBurstLimit', validate: _.isInteger}, + throttlingRateLimit: {prop: 'ThrottlingRateLimit', validate: _.isNumber} }; /** @@ -49,7 +48,7 @@ const internal = { SERVERLESS_STAGE: this._stage } }, - DependsOn: [ deploymentName ] + DependsOn: [deploymentName] }; // Set a reasonable description @@ -88,7 +87,7 @@ const internal = { 'PATCH', 'POST', 'PUT' - ]: [ methodType ]; + ] : [methodType]; _.forOwn(eventStageConfig, (value, key) => { if (!_.has(stageMethodConfigMappings, key)) { @@ -119,11 +118,42 @@ const internal = { } }; -module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { + +function movetoAliasStack(stageStack, resourceType, aliasResources, properties, depends) { + const resources = _.assign({}, _.pickBy(stageStack.Resources, ['Type', resourceType])); + if (!_.isEmpty(resources)) { + const resourceNames = _.keys(resources); + _.forEach(resourceNames, resourceName => { + let obj = resources[resourceName]; + + for (var i in properties) { + obj.Properties[i] = properties[i]; + } + + if (obj.DependsOn) { + if (!obj.DependsOn.push) { + obj.DependsOn = [obj.DependsOn]; + } + } else { + obj.DependsOn = []; + } + + obj.DependsOn = obj.DependsOn.concat(depends); + + aliasResources.push({ + [resourceName]: resources[resourceName] + }); + delete stageStack.Resources[resourceName]; + }); + } +} + + +module.exports = function (currentTemplate, aliasStackTemplates, currentAliasStackTemplate) { const stackName = this._provider.naming.getStackName(); const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate; const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate; - const userResources = _.get(this._serverless.service, 'resources', { Resources: {}, Outputs: {} }); + const userResources = _.get(this._serverless.service, 'resources', {Resources: {}, Outputs: {}}); // Check if our current deployment includes an API deployment let exposeApi = _.includes(_.keys(stageStack.Resources), 'ApiGatewayRestApi'); @@ -131,7 +161,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac if (!exposeApi) { // Check if we have any aliases deployed that reference the API. - if (_.some(aliasStackTemplates, template => _.find(template.Resources, [ 'Type', 'AWS::ApiGateway::Deployment' ]))) { + if (_.some(aliasStackTemplates, template => _.find(template.Resources, ['Type', 'AWS::ApiGateway::Deployment']))) { // Fetch the Api resource from the current stack stageStack.Resources.ApiGatewayRestApi = currentTemplate.Resources.ApiGatewayRestApi; exposeApi = true; @@ -145,7 +175,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac // Export the API for the alias stacks stageStack.Outputs.ApiGatewayRestApi = { Description: 'API Gateway API', - Value: { Ref: 'ApiGatewayRestApi' }, + Value: {Ref: 'ApiGatewayRestApi'}, Export: { Name: `${stackName}-ApiGatewayRestApi` } @@ -154,7 +184,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac // Export the root resource for the API stageStack.Outputs.ApiGatewayRestApiRootResource = { Description: 'API Gateway API root resource', - Value: { 'Fn::GetAtt': [ 'ApiGatewayRestApi', 'RootResourceId' ] }, + Value: {'Fn::GetAtt': ['ApiGatewayRestApi', 'RootResourceId']}, Export: { Name: `${stackName}-ApiGatewayRestApiRootResource` } @@ -164,19 +194,19 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac if (_.some(_.reduce(aliasStackTemplates, (result, template) => { _.merge(result, template.Resources); return result; - }, {}), [ 'Type', 'AWS::ApiGateway::Method' ]) || - _.find(currentAliasStackTemplate.Resources, [ 'Type', 'AWS::ApiGateway::Method' ])) { + }, {}), ['Type', 'AWS::ApiGateway::Method']) || + _.find(currentAliasStackTemplate.Resources, ['Type', 'AWS::ApiGateway::Method'])) { throw new this._serverless.classes.Error('ALIAS PLUGIN ALPHA CHANGE: APIG deployment had to be changed. Please remove the alias stacks and the APIG stage for the alias in CF (AWS console) and redeploy. Sorry!'); } // Move the API deployment into the alias stack. The alias is the owner of the APIG stage. - const deployment = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::ApiGateway::Deployment' ])); + const deployment = _.assign({}, _.pickBy(stageStack.Resources, ['Type', 'AWS::ApiGateway::Deployment'])); if (!_.isEmpty(deployment)) { const deploymentName = _.keys(deployment)[0]; const obj = deployment[deploymentName]; delete obj.Properties.StageName; - obj.Properties.RestApiId = { 'Fn::ImportValue': `${stackName}-ApiGatewayRestApi` }; + obj.Properties.RestApiId = {'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`}; obj.DependsOn = []; aliasResources.push(deployment); @@ -185,36 +215,54 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac // Create stage resource this.options.verbose && this._serverless.cli.log('Configuring stage'); const stageResource = internal.createStageResource.call(this, `${stackName}-ApiGatewayRestApi`, deploymentName); - aliasResources.push({ ApiGatewayStage: stageResource }); + aliasResources.push({ApiGatewayStage: stageResource}); - const baseMapping = _.assign({}, _.pickBy(stageStack.Resources, ['Type', 'AWS::ApiGateway::BasePathMapping'])); - if (!_.isEmpty(baseMapping)) { - const baseMappingName = _.keys(baseMapping)[0]; - const obj = baseMapping[baseMappingName]; - obj.Properties.Stage = { Ref: 'ApiGatewayStage' }; - obj.Properties.RestApiId = { 'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`}; + //Move BasePath mappings + movetoAliasStack(stageStack, 'AWS::ApiGateway::BasePathMapping', aliasResources, { + Stage: {Ref: 'ApiGatewayStage'}, + RestApiId: {'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`} + }, []); - aliasResources.push(baseMapping); - delete stageStack.Resources[baseMappingName]; - } + //Move apiKeys + movetoAliasStack(stageStack, 'AWS::ApiGateway::ApiKey', aliasResources, { + StageKeys: [ + { + RestApiId: {'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`}, + StageName: stageResource.Properties.StageName + } + ] + }, ["ApiGatewayStage"]); + + //Move usageplans + movetoAliasStack(stageStack, 'AWS::ApiGateway::UsagePlan', aliasResources, { + ApiStages: [ + { + ApiId: {'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`}, + Stage: stageResource.Properties.StageName + } + ] + }, ["ApiGatewayStage"]); + + //Move usageplankeys + movetoAliasStack(stageStack, 'AWS::ApiGateway::UsagePlanKey', aliasResources, {}, []); } // Fetch lambda permissions, methods and resources. These have to be updated later to allow the aliased functions. const apiLambdaPermissions = - _.assign({}, - _.pickBy(_.pickBy(stageStack.Resources, [ 'Type', 'AWS::Lambda::Permission' ]), - permission => utils.hasPermissionPrincipal(permission, 'apigateway'))); + _.assign({}, + _.pickBy(_.pickBy(stageStack.Resources, ['Type', 'AWS::Lambda::Permission']), + permission => utils.hasPermissionPrincipal(permission, 'apigateway'))); - const apiMethods = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::ApiGateway::Method' ])); - const authorizers = _.assign({}, _.pickBy(stageStack.Resources, [ 'Type', 'AWS::ApiGateway::Authorizer' ])); - const aliases = _.assign({}, _.pickBy(aliasStack.Resources, [ 'Type', 'AWS::Lambda::Alias' ])); - const versions = _.assign({}, _.pickBy(aliasStack.Resources, [ 'Type', 'AWS::Lambda::Version' ])); + const apiMethods = _.assign({}, _.pickBy(stageStack.Resources, ['Type', 'AWS::ApiGateway::Method'])); + const authorizers = _.assign({}, _.pickBy(stageStack.Resources, ['Type', 'AWS::ApiGateway::Authorizer'])); + const aliases = _.assign({}, _.pickBy(aliasStack.Resources, ['Type', 'AWS::Lambda::Alias'])); + const versions = _.assign({}, _.pickBy(aliasStack.Resources, ['Type', 'AWS::Lambda::Version'])); // Adjust method API and target function _.forOwn(apiMethods, (method, name) => { // Relink to function alias in case we have a lambda endpoint - if (_.includes([ 'AWS', 'AWS_PROXY' ], _.get(method, 'Properties.Integration.Type'))) { + if (_.includes(['AWS', 'AWS_PROXY'], _.get(method, 'Properties.Integration.Type'))) { // For methods it is a bit tricky to find the related function name. There is no direct link. const uriParts = method.Properties.Integration.Uri['Fn::Join'][1]; const funcIndex = _.findIndex(uriParts, part => _.has(part, 'Fn::GetAtt')); @@ -242,7 +290,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac const isExternalRefAuthorizer = _.some(uriParts, isExternalRefAuthorizerPredicate); if (!isExternalRefAuthorizer) { const funcIndex = _.findIndex(uriParts, part => _.startsWith(part, '/invocations')); - uriParts.splice(funcIndex , 0, ':${stageVariables.SERVERLESS_ALIAS}'); + uriParts.splice(funcIndex, 0, ':${stageVariables.SERVERLESS_ALIAS}'); } } @@ -257,7 +305,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac const aliasedName = `${name}${_.replace(this._alias, /-/g, 'Dash')}`; const authorizerRefs = utils.findReferences(stageStack.Resources, name); _.forEach(authorizerRefs, ref => { - _.set(stageStack.Resources, ref, { Ref: aliasedName }); + _.set(stageStack.Resources, ref, {Ref: aliasedName}); }); // Replace dependencies @@ -285,7 +333,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac // Adjust references and alias permissions if (!isExternalRef) { - permission.Properties.FunctionName = { Ref: aliasName }; + permission.Properties.FunctionName = {Ref: aliasName}; } if (permission.Properties.SourceArn) { // Authorizers do not set the SourceArn property @@ -294,13 +342,13 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac '', [ 'arn:', - { Ref: 'AWS::Partition' }, + {Ref: 'AWS::Partition'}, ':execute-api:', - { Ref: 'AWS::Region' }, + {Ref: 'AWS::Region'}, ':', - { Ref: 'AWS::AccountId' }, + {Ref: 'AWS::AccountId'}, ':', - { 'Fn::ImportValue': `${stackName}-ApiGatewayRestApi` }, + {'Fn::ImportValue': `${stackName}-ApiGatewayRestApi`}, '/*/*' ] ] @@ -309,9 +357,9 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac // Add dependency on function version if (!isExternalRef) { - permission.DependsOn = [ versionName, aliasName ]; + permission.DependsOn = [versionName, aliasName]; } else { - permission.DependsOn = _.compact([ versionName, aliasName ]); + permission.DependsOn = _.compact([versionName, aliasName]); } delete stageStack.Resources[name]; @@ -324,7 +372,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac _.forEach(aliasResources, resource => _.assign(aliasStack.Resources, resource)); - return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]); + return Promise.resolve([currentTemplate, aliasStackTemplates, currentAliasStackTemplate]); }; // Exports to make internal functions available for unit tests diff --git a/package-lock.json b/package-lock.json index 18f7832..7204422 100644 --- a/package-lock.json +++ b/package-lock.json @@ -388,9 +388,9 @@ "dev": true }, "acorn-jsx": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz", - "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", "dev": true }, "agent-base": { @@ -2038,9 +2038,9 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.5.1.tgz", - "integrity": "sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -2050,19 +2050,19 @@ "debug": "^4.0.1", "doctrine": "^3.0.0", "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.2", + "eslint-utils": "^1.4.3", "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.1", + "espree": "^6.1.2", "esquery": "^1.0.1", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^5.0.0", - "globals": "^11.7.0", + "globals": "^12.1.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^6.4.1", + "inquirer": "^7.0.0", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", @@ -2071,7 +2071,7 @@ "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "optionator": "^0.8.2", + "optionator": "^0.8.3", "progress": "^2.0.0", "regexpp": "^2.0.1", "semver": "^6.1.2", @@ -2082,12 +2082,134 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { + "ansi-escapes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", + "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "figures": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", + "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "globals": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", + "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "inquirer": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", + "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^2.4.2", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.2.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -2095,7 +2217,21 @@ "dev": true, "requires": { "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -2225,12 +2361,12 @@ } }, "eslint-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", - "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.0.0" + "eslint-visitor-keys": "^1.1.0" } }, "eslint-visitor-keys": { @@ -2250,13 +2386,13 @@ } }, "espree": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.1.tgz", - "integrity": "sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", + "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", "dev": true, "requires": { - "acorn": "^7.0.0", - "acorn-jsx": "^5.0.2", + "acorn": "^7.1.0", + "acorn-jsx": "^5.1.0", "eslint-visitor-keys": "^1.1.0" } }, @@ -2799,9 +2935,9 @@ } }, "fsevents": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.0.7.tgz", - "integrity": "sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", + "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", "dev": true, "optional": true }, @@ -3273,9 +3409,9 @@ "dev": true }, "import-fresh": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", - "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", "dev": true, "requires": { "parent-module": "^1.0.0", @@ -5077,17 +5213,17 @@ } }, "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "dev": true, "requires": { "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", + "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", - "wordwrap": "~1.0.0" + "word-wrap": "~1.2.3" } }, "os": { @@ -7482,10 +7618,10 @@ "string-width": "^2.1.1" } }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, "wrap-ansi": { diff --git a/package.json b/package.json index bc422e8..860c7d2 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "chai-as-promised": "^7.1.1", "chai-subset": "^1.6.0", "coveralls": "^3.0.6", - "eslint": "^6.5.1", + "eslint": "^6.8.0", "eslint-plugin-import": "^2.18.2", "eslint-plugin-lodash": "^6.0.0", "eslint-plugin-promise": "^4.2.1", diff --git a/test/data/auth-stack-2.json b/test/data/auth-stack-2.json index 935cf0d..aaf0242 100644 --- a/test/data/auth-stack-2.json +++ b/test/data/auth-stack-2.json @@ -464,15 +464,103 @@ } } }, - "pathmapping": { - "Type": "AWS::ApiGateway::BasePathMapping", - "Properties": { - "BasePath": "(none)", - "DomainName": "example.com", - "RestApiId": { "Fn::ImportValue": "ApiGatewayRestApi" }, - "Stage": "test" - } - }, + "pathmapping1": { + "Type": "AWS::ApiGateway::BasePathMapping", + "Properties": { + "BasePath": "(none)", + "DomainName": "example.com", + "RestApiId": { "Fn::ImportValue": "ApiGatewayRestApi" }, + "Stage": "test" + } + }, + "pathmapping2": { + "Type": "AWS::ApiGateway::BasePathMapping", + "Properties": { + "BasePath": "/bp2", + "DomainName": "example.com", + "RestApiId": { "Fn::ImportValue": "ApiGatewayRestApi" }, + "Stage": "test" + } + }, + "ApiGatewayApiKey1": { + "Type": "AWS::ApiGateway::ApiKey", + "Properties": { + "Enabled": true, + "Name": "key1", + "StageKeys": [ + { + "RestApiId": { + "Ref": "ApiGatewayRestApi" + }, + "StageName": "dev" + } + ] + }, + "DependsOn": "ApiGatewayDeployment1496754891256" + }, + "ApiGatewayApiKey2": { + "Type": "AWS::ApiGateway::ApiKey", + "Properties": { + "Enabled": true, + "Name": "key2", + "StageKeys": [ + { + "RestApiId": { + "Ref": "ApiGatewayRestApi" + }, + "StageName": "dev" + } + ] + }, + "DependsOn": "ApiGatewayDeployment1496754891256" + }, + "ApiGatewayUsagePlan": { + "Type": "AWS::ApiGateway::UsagePlan", + "DependsOn": "ApiGatewayDeployment1496754891256", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "ApiGatewayRestApi" + }, + "Stage": "dev" + } + ], + "Description": "plan1 description", + "UsagePlanName": "plan1", + "Throttle": { + "BurstLimit": 1, + "RateLimit": 1 + } + } + }, + "ApiGatewayUsagePlanKey1": { + "Type": "AWS::ApiGateway::UsagePlanKey", + "Properties": { + "KeyId": { + "Ref": "ApiGatewayApiKey1" + }, + "KeyType": "API_KEY", + "UsagePlanId": { + "Ref": "ApiGatewayUsagePlan" + } + } + }, + "ApiGatewayUsagePlanKey2": { + "Type": "AWS::ApiGateway::UsagePlanKey", + "Properties": { + "KeyId": { + "Ref": "ApiGatewayApiKey2" + }, + "KeyType": "API_KEY", + "UsagePlanId": { + "Ref": "ApiGatewayUsagePlan" + } + } + }, + + + "TestauthLambdaPermissionApiGateway": { "Type": "AWS::Lambda::Permission", "Properties": { diff --git a/test/stackops/apiGateway.test.js b/test/stackops/apiGateway.test.js index ed04f33..583efc2 100644 --- a/test/stackops/apiGateway.test.js +++ b/test/stackops/apiGateway.test.js @@ -748,9 +748,11 @@ describe('API Gateway', () => { return expect(awsAlias.aliasHandleApiGateway({}, [], {})).to.be.fulfilled .then(()=> BbPromise.all([ expect(template) - .to.not.have.a.nested.property('Resources.pathmapping'), + .to.not.have.a.nested.property('Resources.pathmapping1'), + expect(template) + .to.not.have.a.nested.property('Resources.pathmapping2'), expect(aliasTemplate) - .to.have.a.nested.property('Resources.pathmapping') + .to.have.a.nested.property('Resources.pathmapping1') .that.deep.equals({ Type: 'AWS::ApiGateway::BasePathMapping', Properties: { @@ -758,11 +760,156 @@ describe('API Gateway', () => { DomainName: 'example.com', RestApiId: { 'Fn::ImportValue': 'testService-myStage-ApiGatewayRestApi' }, Stage: { Ref: 'ApiGatewayStage' } - } + }, + DependsOn:[] + }), + expect(aliasTemplate) + .to.have.a.nested.property('Resources.pathmapping2') + .that.deep.equals({ + Type: 'AWS::ApiGateway::BasePathMapping', + Properties: { + BasePath: '/bp2', + DomainName: 'example.com', + RestApiId: { 'Fn::ImportValue': 'testService-myStage-ApiGatewayRestApi' }, + Stage: { Ref: 'ApiGatewayStage' } + }, + DependsOn:[] }) ])); }); + it('should move api keys to alias stack', () => { + stackTemplate = _.cloneDeep(require('../data/auth-stack-2.json')); + const template = serverless.service.provider.compiledCloudFormationTemplate = stackTemplate; + serverless.service.provider.compiledCloudFormationAliasTemplate = aliasTemplate; + return expect(awsAlias.aliasHandleApiGateway({}, [], {})).to.be.fulfilled + .then(()=> BbPromise.all([ + expect(template) + .to.not.have.a.nested.property('Resources.ApiGatewayApiKey1'), + expect(aliasTemplate) + .to.have.a.nested.property('Resources.ApiGatewayApiKey1') + .that.deep.equals( { + Type: 'AWS::ApiGateway::ApiKey', + Properties: { + Enabled: true, + Name: 'key1', + StageKeys: [ + { + RestApiId: { + 'Fn::ImportValue': 'testService-myStage-ApiGatewayRestApi' + }, + StageName: 'myAlias' + } + ] + }, + DependsOn: [ + 'ApiGatewayDeployment1496754891256', + 'ApiGatewayStage' + ] + }), + expect(aliasTemplate) + .to.have.a.nested.property('Resources.ApiGatewayApiKey2') + .that.deep.equals( { + Type: 'AWS::ApiGateway::ApiKey', + Properties: { + Enabled: true, + Name: 'key2', + StageKeys: [ + { + RestApiId: { + 'Fn::ImportValue': 'testService-myStage-ApiGatewayRestApi' + }, + StageName: 'myAlias' + } + ] + }, + DependsOn: [ + 'ApiGatewayDeployment1496754891256', + 'ApiGatewayStage' + ] + }) + ])); + }); + + it('should move usage plans to alias stack', () => { + stackTemplate = _.cloneDeep(require('../data/auth-stack-2.json')); + const template = serverless.service.provider.compiledCloudFormationTemplate = stackTemplate; + serverless.service.provider.compiledCloudFormationAliasTemplate = aliasTemplate; + return expect(awsAlias.aliasHandleApiGateway({}, [], {})).to.be.fulfilled + .then(()=> BbPromise.all([ + expect(template) + .to.not.have.a.nested.property('Resources.ApiGatewayUsagePlan'), + expect(aliasTemplate) + .to.have.a.nested.property('Resources.ApiGatewayUsagePlan') + .that.deep.equals( { + Type: 'AWS::ApiGateway::UsagePlan', + DependsOn: [ + 'ApiGatewayDeployment1496754891256', + 'ApiGatewayStage' + ], + Properties: { + ApiStages: [ + { + ApiId: { + 'Fn::ImportValue': 'testService-myStage-ApiGatewayRestApi' + }, + Stage: 'myAlias' + } + ], + Description: 'plan1 description', + UsagePlanName: 'plan1', + Throttle: { + BurstLimit: 1, + RateLimit: 1 + } + } + }) + ])); + }); + + it('should move api usage plan keys to alias stack', () => { + stackTemplate = _.cloneDeep(require('../data/auth-stack-2.json')); + const template = serverless.service.provider.compiledCloudFormationTemplate = stackTemplate; + serverless.service.provider.compiledCloudFormationAliasTemplate = aliasTemplate; + return expect(awsAlias.aliasHandleApiGateway({}, [], {})).to.be.fulfilled + .then(()=> BbPromise.all([ + expect(template) + .to.not.have.a.nested.property('Resources.ApiGatewayUsagePlanKey1'), + expect(template) + .to.not.have.a.nested.property('Resources.ApiGatewayUsagePlanKey2'), + expect(aliasTemplate) + .to.have.a.nested.property('Resources.ApiGatewayUsagePlanKey1') + .that.deep.equals({ + Type: 'AWS::ApiGateway::UsagePlanKey', + Properties: { + KeyId: { + Ref: 'ApiGatewayApiKey1' + }, + KeyType: 'API_KEY', + UsagePlanId: { + Ref: 'ApiGatewayUsagePlan' + } + }, + DependsOn: [] + }), + expect(aliasTemplate) + .to.have.a.nested.property('Resources.ApiGatewayUsagePlanKey2') + .that.deep.equals({ + Type: 'AWS::ApiGateway::UsagePlanKey', + Properties: { + KeyId: { + Ref: 'ApiGatewayApiKey2' + }, + KeyType: 'API_KEY', + UsagePlanId: { + Ref: 'ApiGatewayUsagePlan' + } + }, + DependsOn: [] + }) + ])); + }); + it('should transform string dependencies and references to authorizers', () => { const template = serverless.service.provider.compiledCloudFormationTemplate = stackTemplate; serverless.service.provider.compiledCloudFormationAliasTemplate = aliasTemplate;