From f513c34df75390813718747d6722d1631c77d1f4 Mon Sep 17 00:00:00 2001 From: Jeff Horner Date: Thu, 24 Sep 2020 11:00:51 +0100 Subject: [PATCH 01/12] feature: Add config option to preserve existing APIGW domains when other path mappings still exist on them --- src/DomainConfig.ts | 2 + src/aws/api-gateway-wrapper.ts | 18 ++++++ src/index.ts | 8 ++- src/types.ts | 1 + test/unit-tests/index.test.ts | 101 +++++++++++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 1 deletion(-) diff --git a/src/DomainConfig.ts b/src/DomainConfig.ts index ebf22254..714d3703 100644 --- a/src/DomainConfig.ts +++ b/src/DomainConfig.ts @@ -25,6 +25,7 @@ class DomainConfig { public securityPolicy: string | undefined; public autoDomain: boolean | undefined; public autoDomainWaitFor: string | undefined; + public preserveExternalPathMappings: boolean | undefined; public domainInfo: DomainInfo | undefined; public apiId: string | undefined; @@ -44,6 +45,7 @@ class DomainConfig { this.allowPathMatching = config.allowPathMatching; this.autoDomain = config.autoDomain; this.autoDomainWaitFor = config.autoDomainWaitFor; + this.preserveExternalPathMappings = config.preserveExternalPathMappings; let basePath = config.basePath; if (basePath == null || basePath.trim() === "") { diff --git a/src/aws/api-gateway-wrapper.ts b/src/aws/api-gateway-wrapper.ts index ea01763a..33614a17 100644 --- a/src/aws/api-gateway-wrapper.ts +++ b/src/aws/api-gateway-wrapper.ts @@ -242,6 +242,24 @@ class APIGatewayWrapper { Globals.logInfo(`Unable to remove basepath mapping for ${domain.givenDomainName}`); } } + + /** + * Checks for presence of external basepath mappings + */ + public async checkExternalPathMappings(domain: DomainConfig): Promise { + Globals.logInfo(`Checking for additional base path mappings on ${domain.givenDomainName}...`); + const mappings: Array = await getAWSPagedResults( + this.apiGatewayV2, + "getApiMappings", + "Items", + "NextToken", + "NextToken", + {DomainName: domain.givenDomainName}, + ); + return mappings.filter(mapping => + !(mapping.ApiId === domain.apiId || (mapping.ApiMappingKey === domain.basePath && domain.allowPathMatching)) + ).length > 0; + } } export = APIGatewayWrapper; diff --git a/src/index.ts b/src/index.ts index bad5dc12..6f586cc2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -305,6 +305,8 @@ class ServerlessCustomDomain { */ public async removeBasePathMappings(): Promise { await Promise.all(this.domains.map(async (domain) => { + const preserveExternalPathMappings = !!domain.preserveExternalPathMappings; + let noExternalPathMappingsOnDomain = !preserveExternalPathMappings; try { domain.apiId = await this.getApiId(domain); @@ -315,7 +317,11 @@ class ServerlessCustomDomain { } else { domain.apiMapping = await this.apiGatewayWrapper.getBasePathMapping(domain); await this.apiGatewayWrapper.deleteBasePathMapping(domain); + if (preserveExternalPathMappings) { + noExternalPathMappingsOnDomain = (await this.apiGatewayWrapper.checkExternalPathMappings(domain) === false); + } } + } catch (err) { if (err.message.indexOf("Failed to find CloudFormation") > -1) { Globals.logInfo(`Unable to find Cloudformation Stack for ${domain.givenDomainName}, @@ -329,7 +335,7 @@ class ServerlessCustomDomain { } const autoDomain = domain.autoDomain; - if (autoDomain === true) { + if (autoDomain === true && noExternalPathMappingsOnDomain) { Globals.logInfo("Deleting domain name after removing base path mapping."); await this.deleteDomain(domain); } diff --git a/src/types.ts b/src/types.ts index a7e2d974..cab84ce8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,7 @@ export interface CustomDomain { // tslint:disable-line autoDomain: boolean | undefined; autoDomainWaitFor: string | undefined; allowPathMatching: boolean | undefined; + preserveExternalPathMappings: boolean | undefined; } export interface ServerlessInstance { // tslint:disable-line diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts index bfbc4198..696036f2 100644 --- a/test/unit-tests/index.test.ts +++ b/test/unit-tests/index.test.ts @@ -55,6 +55,7 @@ const constructPlugin = (customDomainOptions, multiple: boolean = false) => { hostedZonePrivate: customDomainOptions.hostedZonePrivate, securityPolicy: customDomainOptions.securityPolicy, stage: customDomainOptions.stage, + preserveExternalPathMappings: customDomainOptions.preserveExternalPathMappings, }; const serverless = { @@ -1840,7 +1841,107 @@ describe("Custom Domain Plugin", () => { expect(spy).to.have.not.been.called(); }); + it("removeBasePathMapping should not call deleteDomain when preserveExternalPathMappings is true and external mappings exist", async () => { + AWS.mock("CloudFormation", "describeStackResource", (params, callback) => { + callback(null, { + StackResourceDetail: + { + LogicalResourceId: "ApiGatewayRestApi", + PhysicalResourceId: "test_rest_api_id", + }, + }); + }); + AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => { + callback(null, { + Items: [ + {ApiId: "test_rest_api_id", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test"}, + {ApiId: "test_rest_api_id_2", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test"}, + ], + }); + }); + AWS.mock("ApiGatewayV2", "deleteApiMapping", (params, callback) => { + callback(null, params); + }); + AWS.mock("ApiGatewayV2", "deleteDomainName", (params, callback) => { + callback(null, params); + }); + AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => { + callback(null, params); + }); + + const plugin = constructPlugin({ + preserveExternalPathMappings: true, + autoDomain: true, + basePath: "test_basepath", + createRoute53Record: false, + domainName: "test_domain", + restApiId: "test_rest_api_id", + }); + plugin.initializeVariables(); + plugin.initAWSResources(); + + plugin.domains[0].apiMapping = {ApiMappingId: "test_mapping_id"}; + + const spy = chai.spy.on(plugin.apiGatewayWrapper.apiGatewayV2, "deleteDomainName"); + + await plugin.removeBasePathMappings(); + + expect(plugin.serverless.service.custom.customDomain.autoDomain).to.equal(true); + expect(plugin.serverless.service.custom.customDomain.preserveExternalPathMappings).to.equal(true); + expect(spy).to.have.not.been.called(); + }); + + it("removeBasePathMapping should call deleteDomain when preserveExternalPathMappings is true and external mappings don't exist", async () => { + AWS.mock("CloudFormation", "describeStackResource", (params, callback) => { + callback(null, { + StackResourceDetail: + { + LogicalResourceId: "ApiGatewayRestApi", + PhysicalResourceId: "test_rest_api_id", + }, + }); + }); + AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => { + callback(null, { + Items: [ + {ApiId: "test_rest_api_id", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test"}, + ], + }); + }); + AWS.mock("ApiGatewayV2", "deleteApiMapping", (params, callback) => { + callback(null, params); + }); + AWS.mock("ApiGatewayV2", "deleteDomainName", (params, callback) => { + callback(null, params); + }); + AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => { + callback(null, params); + }); + + const plugin = constructPlugin({ + preserveExternalPathMappings: true, + autoDomain: true, + basePath: "test_basepath", + createRoute53Record: false, + domainName: "test_domain", + restApiId: "test_rest_api_id", + }); + plugin.initializeVariables(); + plugin.initAWSResources(); + + plugin.domains[0].apiMapping = {ApiMappingId: "test_mapping_id"}; + + const spy = chai.spy.on(plugin.apiGatewayWrapper.apiGatewayV2, "deleteDomainName"); + + await plugin.removeBasePathMappings(); + + expect(plugin.serverless.service.custom.customDomain.autoDomain).to.equal(true); + expect(plugin.serverless.service.custom.customDomain.preserveExternalPathMappings).to.equal(true); + expect(spy).to.have.been.called(); + }); + afterEach(() => { + AWS.restore(); consoleOutput = []; }); }); From 3ccbcf0b11313121e063f6d2d7388b824f6d32e2 Mon Sep 17 00:00:00 2001 From: Jeff Horner Date: Thu, 24 Sep 2020 11:37:13 +0100 Subject: [PATCH 02/12] chore: README update --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bb0a338e..c30efc7d 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ securityPolicy | tls_1_2 | The security policy to apply to the custom domain nam allowPathMatching | false | When updating an existing api mapping this will match on the basePath instead of the API ID to find existing mappings for an upsate. This should only be used when changing API types. For example, migrating a REST API to an HTTP API. See Changing API Types for more information. | | autoDomain | `false` | Toggles whether or not the plugin will run `create_domain/delete_domain` as part of `sls deploy/remove` so that multiple commands are not required. | | autoDomainWaitFor | `120` | How long to wait for create_domain to finish before starting deployment if domain does not exist immediately. | +| preserveExternalPathMappings | `false` | When `autoDomain` is set to true, and a deployment is removed, setting this wto `true` checks for additional API Gateway base path mappings before automatically deleting the domain, and avoids doing so if they exist. | From 7b9432b33b8fdccb0799802f33586346620e3949 Mon Sep 17 00:00:00 2001 From: Jeff Horner Date: Thu, 24 Sep 2020 12:01:45 +0100 Subject: [PATCH 03/12] fix: Lint --- src/aws/api-gateway-wrapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aws/api-gateway-wrapper.ts b/src/aws/api-gateway-wrapper.ts index 33614a17..ab12473a 100644 --- a/src/aws/api-gateway-wrapper.ts +++ b/src/aws/api-gateway-wrapper.ts @@ -248,7 +248,7 @@ class APIGatewayWrapper { */ public async checkExternalPathMappings(domain: DomainConfig): Promise { Globals.logInfo(`Checking for additional base path mappings on ${domain.givenDomainName}...`); - const mappings: Array = await getAWSPagedResults( + const mappings = await getAWSPagedResults( this.apiGatewayV2, "getApiMappings", "Items", From e3338689cc6164742f8bf359ec37d972f55bfb6d Mon Sep 17 00:00:00 2001 From: Jeff Horner Date: Thu, 24 Sep 2020 12:10:20 +0100 Subject: [PATCH 04/12] chore: Codacy issues --- src/aws/api-gateway-wrapper.ts | 6 ++++-- test/unit-tests/index.test.ts | 19 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/aws/api-gateway-wrapper.ts b/src/aws/api-gateway-wrapper.ts index ab12473a..f7cdc452 100644 --- a/src/aws/api-gateway-wrapper.ts +++ b/src/aws/api-gateway-wrapper.ts @@ -256,8 +256,10 @@ class APIGatewayWrapper { "NextToken", {DomainName: domain.givenDomainName}, ); - return mappings.filter(mapping => - !(mapping.ApiId === domain.apiId || (mapping.ApiMappingKey === domain.basePath && domain.allowPathMatching)) + return mappings.filter((mapping) => + !(mapping.ApiId === domain.apiId || + (mapping.ApiMappingKey === domain.basePath && domain.allowPathMatching) + ), ).length > 0; } } diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts index 696036f2..b4cde705 100644 --- a/test/unit-tests/index.test.ts +++ b/test/unit-tests/index.test.ts @@ -53,9 +53,9 @@ const constructPlugin = (customDomainOptions, multiple: boolean = false) => { endpointType: customDomainOptions.endpointType, hostedZoneId: customDomainOptions.hostedZoneId, hostedZonePrivate: customDomainOptions.hostedZonePrivate, + preserveExternalPathMappings: customDomainOptions.preserveExternalPathMappings, securityPolicy: customDomainOptions.securityPolicy, stage: customDomainOptions.stage, - preserveExternalPathMappings: customDomainOptions.preserveExternalPathMappings, }; const serverless = { @@ -1841,7 +1841,8 @@ describe("Custom Domain Plugin", () => { expect(spy).to.have.not.been.called(); }); - it("removeBasePathMapping should not call deleteDomain when preserveExternalPathMappings is true and external mappings exist", async () => { + it("removeBasePathMapping should not call deleteDomain when preserveExternalPathMappings is true and " + + "external mappings exist", async () => { AWS.mock("CloudFormation", "describeStackResource", (params, callback) => { callback(null, { StackResourceDetail: @@ -1855,7 +1856,12 @@ describe("Custom Domain Plugin", () => { callback(null, { Items: [ {ApiId: "test_rest_api_id", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test"}, - {ApiId: "test_rest_api_id_2", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test"}, + { + ApiId: "test_rest_api_id_2", + MappingKey: "test", + ApiMappingId: "test_mapping_id", + Stage: "test" + }, ], }); }); @@ -1870,11 +1876,11 @@ describe("Custom Domain Plugin", () => { }); const plugin = constructPlugin({ - preserveExternalPathMappings: true, autoDomain: true, basePath: "test_basepath", createRoute53Record: false, domainName: "test_domain", + preserveExternalPathMappings: true, restApiId: "test_rest_api_id", }); plugin.initializeVariables(); @@ -1891,7 +1897,8 @@ describe("Custom Domain Plugin", () => { expect(spy).to.have.not.been.called(); }); - it("removeBasePathMapping should call deleteDomain when preserveExternalPathMappings is true and external mappings don't exist", async () => { + it("removeBasePathMapping should call deleteDomain when preserveExternalPathMappings is true and " + + "external mappings don't exist", async () => { AWS.mock("CloudFormation", "describeStackResource", (params, callback) => { callback(null, { StackResourceDetail: @@ -1919,11 +1926,11 @@ describe("Custom Domain Plugin", () => { }); const plugin = constructPlugin({ - preserveExternalPathMappings: true, autoDomain: true, basePath: "test_basepath", createRoute53Record: false, domainName: "test_domain", + preserveExternalPathMappings: true, restApiId: "test_rest_api_id", }); plugin.initializeVariables(); From 6778a66af1a6470347e75cdad2c8d03ca2d4af68 Mon Sep 17 00:00:00 2001 From: Jeff Horner Date: Thu, 24 Sep 2020 12:14:17 +0100 Subject: [PATCH 05/12] chore: Codacy issues --- src/index.ts | 4 +++- test/unit-tests/index.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6f586cc2..e337f775 100644 --- a/src/index.ts +++ b/src/index.ts @@ -318,7 +318,9 @@ class ServerlessCustomDomain { domain.apiMapping = await this.apiGatewayWrapper.getBasePathMapping(domain); await this.apiGatewayWrapper.deleteBasePathMapping(domain); if (preserveExternalPathMappings) { - noExternalPathMappingsOnDomain = (await this.apiGatewayWrapper.checkExternalPathMappings(domain) === false); + noExternalPathMappingsOnDomain = ( + await this.apiGatewayWrapper.checkExternalPathMappings(domain) === false + ); } } diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts index b4cde705..f9962bb8 100644 --- a/test/unit-tests/index.test.ts +++ b/test/unit-tests/index.test.ts @@ -1858,9 +1858,9 @@ describe("Custom Domain Plugin", () => { {ApiId: "test_rest_api_id", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test"}, { ApiId: "test_rest_api_id_2", - MappingKey: "test", ApiMappingId: "test_mapping_id", - Stage: "test" + MappingKey: "test", + Stage: "test", }, ], }); From df7b6d566692c061e1caf73e28b63b31cdaa18e6 Mon Sep 17 00:00:00 2001 From: Dima Revutskyi Date: Tue, 25 Jan 2022 13:55:00 +0200 Subject: [PATCH 06/12] update dependences and tests --- package-lock.json | 102 +++++++++++++++++----------------- package.json | 4 +- test/unit-tests/index.test.ts | 4 ++ 3 files changed, 58 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index c0b58c22..51577a74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.0", - "@types/node": "^17.0.10", + "@types/node": "^17.0.12", "aws-sdk-mock": "^5.6.0", "chai": "^4.3.4", "chai-spies": "^1.0.0", @@ -24,7 +24,7 @@ "randomstring": "^1.2.2", "request": "^2.88.2", "request-promise-native": "^1.0.9", - "serverless": "^2.72.1", + "serverless": "^2.72.2", "serverless-plugin-split-stacks": "^1.11.3", "shelljs": "^0.8.5", "ts-node": "^10.4.0", @@ -1373,9 +1373,9 @@ } }, "node_modules/@sindresorhus/is": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.3.0.tgz", - "integrity": "sha512-wwOvh0eO3PiTEivGJWiZ+b946SlMSb4pe+y+Ur/4S87cwo09pYi+FWHHnbrM3W9W7cBYKDqQXcrFYjYUCOJUEQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.4.0.tgz", + "integrity": "sha512-QppPM/8l3Mawvh4rn9CNEYIU9bxpXUCRMaX9yUpvBk1nMKusLKpfXGDEKExKaPhLzcn3lzil7pR6rnJ11HgeRQ==", "dev": true, "engines": { "node": ">=10" @@ -1525,9 +1525,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.10.tgz", - "integrity": "sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog==", + "version": "17.0.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.12.tgz", + "integrity": "sha512-4YpbAsnJXWYK/fpTVFlMIcUIho2AYCi4wg5aNPrG1ng7fn/1/RZfCIpRCiBX+12RVa34RluilnvCqD+g3KiSiA==", "dev": true }, "node_modules/@types/request": { @@ -3019,13 +3019,13 @@ "dev": true }, "node_modules/crc-32": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", - "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.1.tgz", + "integrity": "sha512-Dn/xm/1vFFgs3nfrpEVScHoIslO9NZRITWGz/1E/St6u4xw99vfZzVkW0OSnzx2h9egej9xwMCEut6sqwokM/w==", "dev": true, "dependencies": { "exit-on-epipe": "~1.0.1", - "printj": "~1.1.0" + "printj": "~1.3.1" }, "bin": { "crc32": "bin/crc32.njs" @@ -3591,9 +3591,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.51", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.51.tgz", - "integrity": "sha512-JNEmcYl3mk1tGQmy0EvL5eik/CKSBuzAyGP0QFdG6LIgxQe3II0BL1m2zKc2MZMf3uGqHWE1TFddJML0RpjSHQ==", + "version": "1.4.52", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.52.tgz", + "integrity": "sha512-JGkh8HEh5PnVrhU4HbpyyO0O791dVY6k7AdqfDeqbcRMeoGxtNHWT77deR2nhvbLe4dKpxjlDEvdEwrvRLGu2Q==", "dev": true }, "node_modules/emoji-regex": { @@ -5971,15 +5971,16 @@ "dev": true }, "node_modules/ncjsm": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ncjsm/-/ncjsm-4.2.0.tgz", - "integrity": "sha512-L2Qij4PTy7Bs4TB24zs7FLIAYJTaR5JPvSig5hIcO059LnMCNgy6MfHHNyg8s/aekPKrTqKX90gBGt3NNGvhdw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ncjsm/-/ncjsm-4.3.0.tgz", + "integrity": "sha512-oah6YGwb4Ern2alojiMFcjPhE4wvQBw1Ur/kUr2P0ovKdzaF5pCIsGjs0f2y+iZeej0/5Y6OOhQ8j30cTDMEGw==", "dev": true, "dependencies": { "builtin-modules": "^3.2.0", "deferred": "^0.7.11", "es5-ext": "^0.10.53", "es6-set": "^0.1.5", + "ext": "^1.6.0", "find-requires": "^1.0.0", "fs2": "^0.3.9", "type": "^2.5.0" @@ -7049,9 +7050,9 @@ "dev": true }, "node_modules/printj": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", - "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.3.1.tgz", + "integrity": "sha512-GA3TdL8szPK4AQ2YnOe/b+Y1jUFwmmGMMK/qbY7VcE3Z7FU8JstbKiKRzO6CIiAKPhTO8m01NoQ0V5f3jc4OGg==", "dev": true, "bin": { "printj": "bin/printj.njs" @@ -7682,9 +7683,9 @@ } }, "node_modules/serverless": { - "version": "2.72.1", - "resolved": "https://registry.npmjs.org/serverless/-/serverless-2.72.1.tgz", - "integrity": "sha512-SxmxyBgWQvcKvEXdP0fR3Y+nljOQ+nWCPDZXnhic//w+k0kNQ/bHcd3S1VZQqU3m7nZZwMsdC5lxB26EpUFELA==", + "version": "2.72.2", + "resolved": "https://registry.npmjs.org/serverless/-/serverless-2.72.2.tgz", + "integrity": "sha512-xlxaWyq4b58DIX3prwIikBmXj0z4R+YI9zcDPYgQc78mKJ0qTgCK7BMoy6dlVuHXnfhBkhT3uT/EhFvjKIdk6g==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -7696,7 +7697,7 @@ "ajv": "^6.12.6", "ajv-keywords": "^3.5.2", "archiver": "^5.3.0", - "aws-sdk": "^2.1061.0", + "aws-sdk": "^2.1062.0", "bluebird": "^3.7.2", "boxen": "^5.1.2", "cachedir": "^2.3.0", @@ -7726,7 +7727,7 @@ "lodash": "^4.17.21", "memoizee": "^0.4.15", "micromatch": "^4.0.4", - "ncjsm": "^4.2.0", + "ncjsm": "^4.3.0", "node-fetch": "^2.6.7", "open": "^7.4.2", "path2": "^0.1.0", @@ -11007,9 +11008,9 @@ } }, "@sindresorhus/is": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.3.0.tgz", - "integrity": "sha512-wwOvh0eO3PiTEivGJWiZ+b946SlMSb4pe+y+Ur/4S87cwo09pYi+FWHHnbrM3W9W7cBYKDqQXcrFYjYUCOJUEQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.4.0.tgz", + "integrity": "sha512-QppPM/8l3Mawvh4rn9CNEYIU9bxpXUCRMaX9yUpvBk1nMKusLKpfXGDEKExKaPhLzcn3lzil7pR6rnJ11HgeRQ==", "dev": true }, "@sinonjs/commons": { @@ -11150,9 +11151,9 @@ "dev": true }, "@types/node": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.10.tgz", - "integrity": "sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog==", + "version": "17.0.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.12.tgz", + "integrity": "sha512-4YpbAsnJXWYK/fpTVFlMIcUIho2AYCi4wg5aNPrG1ng7fn/1/RZfCIpRCiBX+12RVa34RluilnvCqD+g3KiSiA==", "dev": true }, "@types/request": { @@ -12360,13 +12361,13 @@ "dev": true }, "crc-32": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", - "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.1.tgz", + "integrity": "sha512-Dn/xm/1vFFgs3nfrpEVScHoIslO9NZRITWGz/1E/St6u4xw99vfZzVkW0OSnzx2h9egej9xwMCEut6sqwokM/w==", "dev": true, "requires": { "exit-on-epipe": "~1.0.1", - "printj": "~1.1.0" + "printj": "~1.3.1" } }, "crc32-stream": { @@ -12828,9 +12829,9 @@ } }, "electron-to-chromium": { - "version": "1.4.51", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.51.tgz", - "integrity": "sha512-JNEmcYl3mk1tGQmy0EvL5eik/CKSBuzAyGP0QFdG6LIgxQe3II0BL1m2zKc2MZMf3uGqHWE1TFddJML0RpjSHQ==", + "version": "1.4.52", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.52.tgz", + "integrity": "sha512-JGkh8HEh5PnVrhU4HbpyyO0O791dVY6k7AdqfDeqbcRMeoGxtNHWT77deR2nhvbLe4dKpxjlDEvdEwrvRLGu2Q==", "dev": true }, "emoji-regex": { @@ -14728,15 +14729,16 @@ "dev": true }, "ncjsm": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ncjsm/-/ncjsm-4.2.0.tgz", - "integrity": "sha512-L2Qij4PTy7Bs4TB24zs7FLIAYJTaR5JPvSig5hIcO059LnMCNgy6MfHHNyg8s/aekPKrTqKX90gBGt3NNGvhdw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ncjsm/-/ncjsm-4.3.0.tgz", + "integrity": "sha512-oah6YGwb4Ern2alojiMFcjPhE4wvQBw1Ur/kUr2P0ovKdzaF5pCIsGjs0f2y+iZeej0/5Y6OOhQ8j30cTDMEGw==", "dev": true, "requires": { "builtin-modules": "^3.2.0", "deferred": "^0.7.11", "es5-ext": "^0.10.53", "es6-set": "^0.1.5", + "ext": "^1.6.0", "find-requires": "^1.0.0", "fs2": "^0.3.9", "type": "^2.5.0" @@ -15575,9 +15577,9 @@ } }, "printj": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", - "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.3.1.tgz", + "integrity": "sha512-GA3TdL8szPK4AQ2YnOe/b+Y1jUFwmmGMMK/qbY7VcE3Z7FU8JstbKiKRzO6CIiAKPhTO8m01NoQ0V5f3jc4OGg==", "dev": true }, "process-nextick-args": { @@ -16040,9 +16042,9 @@ } }, "serverless": { - "version": "2.72.1", - "resolved": "https://registry.npmjs.org/serverless/-/serverless-2.72.1.tgz", - "integrity": "sha512-SxmxyBgWQvcKvEXdP0fR3Y+nljOQ+nWCPDZXnhic//w+k0kNQ/bHcd3S1VZQqU3m7nZZwMsdC5lxB26EpUFELA==", + "version": "2.72.2", + "resolved": "https://registry.npmjs.org/serverless/-/serverless-2.72.2.tgz", + "integrity": "sha512-xlxaWyq4b58DIX3prwIikBmXj0z4R+YI9zcDPYgQc78mKJ0qTgCK7BMoy6dlVuHXnfhBkhT3uT/EhFvjKIdk6g==", "dev": true, "requires": { "@serverless/cli": "^1.6.0", @@ -16053,7 +16055,7 @@ "ajv": "^6.12.6", "ajv-keywords": "^3.5.2", "archiver": "^5.3.0", - "aws-sdk": "^2.1061.0", + "aws-sdk": "^2.1062.0", "bluebird": "^3.7.2", "boxen": "^5.1.2", "cachedir": "^2.3.0", @@ -16083,7 +16085,7 @@ "lodash": "^4.17.21", "memoizee": "^0.4.15", "micromatch": "^4.0.4", - "ncjsm": "^4.2.0", + "ncjsm": "^4.3.0", "node-fetch": "^2.6.7", "open": "^7.4.2", "path2": "^0.1.0", diff --git a/package.json b/package.json index 0fd1679b..a36a6717 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.0", - "@types/node": "^17.0.10", + "@types/node": "^17.0.12", "aws-sdk-mock": "^5.6.0", "chai": "^4.3.4", "chai-spies": "^1.0.0", @@ -57,7 +57,7 @@ "randomstring": "^1.2.2", "request": "^2.88.2", "request-promise-native": "^1.0.9", - "serverless": "^2.72.1", + "serverless": "^2.72.2", "serverless-plugin-split-stacks": "^1.11.3", "shelljs": "^0.8.5", "ts-node": "^10.4.0", diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts index e80ea805..5352ff95 100644 --- a/test/unit-tests/index.test.ts +++ b/test/unit-tests/index.test.ts @@ -1942,6 +1942,7 @@ describe("Custom Domain Plugin", () => { it("removeBasePathMapping should not call deleteDomain when preserveExternalPathMappings is true and " + "external mappings exist", async () => { AWS.mock("CloudFormation", "describeStackResource", (params, callback) => { + // @ts-ignore callback(null, { StackResourceDetail: { @@ -1951,6 +1952,7 @@ describe("Custom Domain Plugin", () => { }); }); AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => { + // @ts-ignore callback(null, { Items: [ {ApiId: "test_rest_api_id", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test"}, @@ -1998,6 +2000,7 @@ describe("Custom Domain Plugin", () => { it("removeBasePathMapping should call deleteDomain when preserveExternalPathMappings is true and " + "external mappings don't exist", async () => { AWS.mock("CloudFormation", "describeStackResource", (params, callback) => { + // @ts-ignore callback(null, { StackResourceDetail: { @@ -2007,6 +2010,7 @@ describe("Custom Domain Plugin", () => { }); }); AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => { + // @ts-ignore callback(null, { Items: [ {ApiId: "test_rest_api_id", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test"}, From d63cd7d42197b6ef601fc604bc429af936847ae8 Mon Sep 17 00:00:00 2001 From: Dima Revutskyi Date: Tue, 25 Jan 2022 16:47:45 +0200 Subject: [PATCH 07/12] refactoring --- src/aws/api-gateway-wrapper.ts | 33 ++------------------------------- src/domain-config.ts | 2 +- src/index.ts | 33 +++++++++++++++++++++------------ test/unit-tests/index.test.ts | 33 ++------------------------------- 4 files changed, 26 insertions(+), 75 deletions(-) diff --git a/src/aws/api-gateway-wrapper.ts b/src/aws/api-gateway-wrapper.ts index 0c86962b..a9b9cbee 100644 --- a/src/aws/api-gateway-wrapper.ts +++ b/src/aws/api-gateway-wrapper.ts @@ -148,12 +148,9 @@ class APIGatewayWrapper { } } - /** - * Get basepath mapping - */ - public async getBasePathMapping(domain: DomainConfig): Promise { + public async getApiMappings(domain: DomainConfig): Promise { try { - const mappings = await getAWSPagedResults( + return await getAWSPagedResults( this.apiGatewayV2, "getApiMappings", "Items", @@ -161,12 +158,6 @@ class APIGatewayWrapper { "NextToken", {DomainName: domain.givenDomainName}, ); - for (const mapping of mappings) { - if (mapping.ApiId === domain.apiId - || (mapping.ApiMappingKey === domain.basePath && domain.allowPathMatching)) { - return mapping; - } - } } catch (err) { Globals.logError(err, domain.givenDomainName); throw new Error(`Unable to get API Mappings for ${domain.givenDomainName}`); @@ -243,26 +234,6 @@ class APIGatewayWrapper { Globals.logInfo(`Unable to remove basepath mapping for ${domain.givenDomainName}`); } } - - /** - * Checks for presence of external basepath mappings - */ - public async checkExternalPathMappings(domain: DomainConfig): Promise { - Globals.logInfo(`Checking for additional base path mappings on ${domain.givenDomainName}...`); - const mappings = await getAWSPagedResults( - this.apiGatewayV2, - "getApiMappings", - "Items", - "NextToken", - "NextToken", - {DomainName: domain.givenDomainName}, - ); - return mappings.filter((mapping) => - !(mapping.ApiId === domain.apiId || - (mapping.ApiMappingKey === domain.basePath && domain.allowPathMatching) - ), - ).length > 0; - } } export = APIGatewayWrapper; diff --git a/src/domain-config.ts b/src/domain-config.ts index 0c2839b0..4187af15 100644 --- a/src/domain-config.ts +++ b/src/domain-config.ts @@ -51,7 +51,7 @@ class DomainConfig { this.allowPathMatching = config.allowPathMatching; this.autoDomain = config.autoDomain; this.autoDomainWaitFor = config.autoDomainWaitFor; - this.preserveExternalPathMappings = config.preserveExternalPathMappings; + this.preserveExternalPathMappings = this.evaluateBoolean(config.preserveExternalPathMappings, false); let basePath = config.basePath; if (basePath == null || basePath.trim() === "") { diff --git a/src/index.ts b/src/index.ts index b87a6464..55e2be44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -300,8 +300,15 @@ class ServerlessCustomDomain { public async setupBasePathMappings(): Promise { await Promise.all(this.domains.map(async (domain) => { try { + const mappings = await this.apiGatewayWrapper.getApiMappings(domain); + const filteredMappings = mappings.filter((mapping) => { + return mapping.ApiId === domain.apiId || ( + mapping.ApiMappingKey === domain.basePath && domain.allowPathMatching + ) + }); + domain.apiId = await this.getApiId(domain); - domain.apiMapping = await this.apiGatewayWrapper.getBasePathMapping(domain); + domain.apiMapping = filteredMappings ? filteredMappings[0] : null; domain.domainInfo = await this.apiGatewayWrapper.getCustomDomainInfo(domain); if (!domain.apiMapping) { @@ -328,8 +335,7 @@ class ServerlessCustomDomain { */ public async removeBasePathMappings(): Promise { await Promise.all(this.domains.map(async (domain) => { - const preserveExternalPathMappings = !!domain.preserveExternalPathMappings; - let noExternalPathMappingsOnDomain = !preserveExternalPathMappings; + let externalBasePathExists = false; try { domain.apiId = await this.getApiId(domain); @@ -338,15 +344,19 @@ class ServerlessCustomDomain { Globals.logInfo(`Unable to find corresponding API for ${domain.givenDomainName}, API Mappings may need to be manually removed.`); } else { - domain.apiMapping = await this.apiGatewayWrapper.getBasePathMapping(domain); - await this.apiGatewayWrapper.deleteBasePathMapping(domain); - if (preserveExternalPathMappings) { - noExternalPathMappingsOnDomain = ( - await this.apiGatewayWrapper.checkExternalPathMappings(domain) === false - ); + const mappings = await this.apiGatewayWrapper.getApiMappings(domain); + const filteredMappings = mappings.filter((mapping) => { + return mapping.ApiId === domain.apiId || ( + mapping.ApiMappingKey === domain.basePath && domain.allowPathMatching + ) + }); + if (domain.preserveExternalPathMappings) { + externalBasePathExists = mappings.length > filteredMappings.length; } - } + domain.apiMapping = filteredMappings ? filteredMappings[0] : null; + await this.apiGatewayWrapper.deleteBasePathMapping(domain); + } } catch (err) { if (err.message.indexOf("Failed to find CloudFormation") > -1) { Globals.logInfo(`Unable to find Cloudformation Stack for ${domain.givenDomainName}, @@ -359,8 +369,7 @@ class ServerlessCustomDomain { } } - const autoDomain = domain.autoDomain; - if (autoDomain === true && noExternalPathMappingsOnDomain) { + if (domain.autoDomain === true && !externalBasePathExists) { Globals.logInfo("Deleting domain name after removing base path mapping."); await this.deleteDomain(domain); } diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts index 5352ff95..bc4df785 100644 --- a/test/unit-tests/index.test.ts +++ b/test/unit-tests/index.test.ts @@ -732,35 +732,6 @@ describe("Custom Domain Plugin", () => { }); describe("Gets existing basepath mappings correctly", () => { - it("Returns undefined if no basepaths map to current api", async () => { - AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => { - // @ts-ignore - callback(null, { - Items: [ - { - ApiId: "someother_api_id", - ApiMappingId: "test_rest_api_id_one", - MappingKey: "test", - Stage: "test", - }, - ], - }); - }); - - const plugin = constructPlugin({ - domainName: "test_domain", - }); - - const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); - dc.apiMapping = {ApiMappingId: "api_id"}; - - plugin.initializeVariables(); - plugin.initAWSResources(); - - const result = await plugin.apiGatewayWrapper.getBasePathMapping(dc); - expect(result).to.equal(undefined); - }); - it("Returns current api mapping", async () => { AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => { callback(null, { @@ -782,8 +753,8 @@ describe("Custom Domain Plugin", () => { plugin.initializeVariables(); plugin.initAWSResources(); - const result = await plugin.apiGatewayWrapper.getBasePathMapping(dc); - expect(result).to.eql({ + const result = await plugin.apiGatewayWrapper.getApiMappings(dc); + expect(result[0]).to.eql({ ApiId: "test_rest_api_id", ApiMappingId: "fake_id", ApiMappingKey: "api", From 1d60a183538f99e4190b5cedb95dd16439f37949 Mon Sep 17 00:00:00 2001 From: Dima Revutskyi Date: Tue, 25 Jan 2022 17:07:31 +0200 Subject: [PATCH 08/12] Update change log --- CHANGELOG.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ef642d0..c6a06b60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [5.6.0] - 2021-01-25 +- Added config option to avoid automatically deleting an APIGW domain when other base path mappings exist. Thank you @straticJeff ([389](https://github.com/amplify-education/serverless-domain-manager/pull/389)) + ## [5.5.0] - 2021-01-24 - Added proxy support. Thank you @mscharp ([405](https://github.com/amplify-education/serverless-domain-manager/pull/405)) - Fixed issue with disabling createRoute53Record. Thank you @albinlundmark ([476](https://github.com/amplify-education/serverless-domain-manager/pull/476)) diff --git a/package.json b/package.json index a36a6717..2cb299d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless-domain-manager", - "version": "5.5.0", + "version": "5.6.0", "engines": { "node": ">=12" }, From 2e5e46d7bed2a63d0c4fa60f0cafed426c5e67e6 Mon Sep 17 00:00:00 2001 From: Dima Revutskyi Date: Wed, 26 Jan 2022 12:25:16 +0200 Subject: [PATCH 09/12] Fix getApiMappings --- src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 55e2be44..2ef904b0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -300,14 +300,13 @@ class ServerlessCustomDomain { public async setupBasePathMappings(): Promise { await Promise.all(this.domains.map(async (domain) => { try { + domain.apiId = await this.getApiId(domain); const mappings = await this.apiGatewayWrapper.getApiMappings(domain); const filteredMappings = mappings.filter((mapping) => { return mapping.ApiId === domain.apiId || ( mapping.ApiMappingKey === domain.basePath && domain.allowPathMatching ) }); - - domain.apiId = await this.getApiId(domain); domain.apiMapping = filteredMappings ? filteredMappings[0] : null; domain.domainInfo = await this.apiGatewayWrapper.getCustomDomainInfo(domain); From 4ed5ffa8a4a06d1db8f67d5ccac27db79861bcec Mon Sep 17 00:00:00 2001 From: Dima Revutskyi Date: Wed, 26 Jan 2022 12:26:01 +0200 Subject: [PATCH 10/12] update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a06b60..e4fbf52f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [5.6.0] - 2021-01-25 +## [5.6.0] - 2021-01-26 - Added config option to avoid automatically deleting an APIGW domain when other base path mappings exist. Thank you @straticJeff ([389](https://github.com/amplify-education/serverless-domain-manager/pull/389)) ## [5.5.0] - 2021-01-24 From 8154ac1791f4ade7e4be4bcae097a75c4051a733 Mon Sep 17 00:00:00 2001 From: Dima Revutskyi Date: Wed, 26 Jan 2022 12:30:52 +0200 Subject: [PATCH 11/12] update test --- test/integration-tests/basic.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/integration-tests/basic.test.ts b/test/integration-tests/basic.test.ts index 7fe8fdf6..a1bd96de 100644 --- a/test/integration-tests/basic.test.ts +++ b/test/integration-tests/basic.test.ts @@ -94,7 +94,10 @@ describe("Integration Tests", function () { expect(basePath).to.equal("hello-world"); } finally { // should destroy the last created config folder ( import config ) - await utilities.destroyResources(`${testExportName} & ${testImportName}`); + await utilities.destroyResources(testImportName); + // temp dir are empty and we need to update it with export config for the proper clenup + await utilities.createTempDir(TEMP_DIR, configExportFolder); + await utilities.destroyResources(testExportName); } }); From 9fd78a83447ebb6fe5911e4b833c802a137fc0aa Mon Sep 17 00:00:00 2001 From: Dima Revutskyi Date: Wed, 26 Jan 2022 12:31:24 +0200 Subject: [PATCH 12/12] fix text --- test/integration-tests/basic.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration-tests/basic.test.ts b/test/integration-tests/basic.test.ts index a1bd96de..c749cad0 100644 --- a/test/integration-tests/basic.test.ts +++ b/test/integration-tests/basic.test.ts @@ -95,7 +95,7 @@ describe("Integration Tests", function () { } finally { // should destroy the last created config folder ( import config ) await utilities.destroyResources(testImportName); - // temp dir are empty and we need to update it with export config for the proper clenup + // temp dir are empty and we need to update it with export config for the proper cleanup await utilities.createTempDir(TEMP_DIR, configExportFolder); await utilities.destroyResources(testExportName); }