diff --git a/CHANGELOG.md b/CHANGELOG.md index c66cf1c5..46b0b652 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ 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). +## [7.3.5] - 2023-02-06 + +### Fixed +- Hardcoded `$default` stage for the HTTP API and default base path. More info [here](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-stages.html) + ## [7.3.4] - 2023-01-30 ### Fixed diff --git a/dependabot.yml b/dependabot.yml deleted file mode 100644 index aff82a10..00000000 --- a/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" diff --git a/package-lock.json b/package-lock.json index a97d45bf..fa742fcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "serverless-domain-manager", - "version": "7.3.3", + "version": "7.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "serverless-domain-manager", - "version": "7.3.3", + "version": "7.3.4", "license": "MIT", "dependencies": { "@aws-sdk/client-acm": "^3.460.0", @@ -1709,18 +1709,6 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@eslint-community/regexpp": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", @@ -1786,9 +1774,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -3523,18 +3511,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/2-thenable": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/2-thenable/-/2-thenable-1.0.0.tgz", @@ -3558,9 +3534,9 @@ } }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -6001,12 +5977,15 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { - "node": ">=10" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/@babel/code-frame": { @@ -6053,6 +6032,15 @@ "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint/node_modules/globals": { "version": "13.23.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", @@ -6121,9 +6109,9 @@ } }, "node_modules/eslint/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" diff --git a/package.json b/package.json index bb66bf99..111ee794 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless-domain-manager", - "version": "7.3.4", + "version": "7.3.5", "engines": { "node": ">=14" }, @@ -29,7 +29,6 @@ "scripts": { "integration-basic": "nyc mocha -r ts-node/register --project tsconfig.json test/integration-tests/basic/basic.test.ts", "integration-deploy": "nyc mocha -r ts-node/register --project tsconfig.json test/integration-tests/deploy/deploy.test.ts", - "integration-debug": "nyc mocha -r ts-node/register --project tsconfig.json test/integration-tests/debug/debug.test.ts", "test": "find ./test/unit-tests -name '*.test.ts' | xargs nyc mocha -r ts-node/register --project tsconfig.json --timeout 5000 && nyc report --reporter=text-summary", "test:debug": "NODE_OPTIONS='--inspect-brk' mocha -j 1 -r ts-node/register --project tsconfig.json test/unit-tests/index.test.ts", "integration-test": "npm run integration-basic && npm run integration-deploy", diff --git a/src/aws/acm-wrapper.ts b/src/aws/acm-wrapper.ts index 7d8ea72f..2ec21b67 100644 --- a/src/aws/acm-wrapper.ts +++ b/src/aws/acm-wrapper.ts @@ -18,81 +18,81 @@ const certStatuses = [ ]; class ACMWrapper { - public acm: ACMClient; + public acm: ACMClient; - constructor (credentials: any, endpointType: string) { - const isEdge = endpointType === Globals.endpointTypes.edge; - this.acm = new ACMClient({ - credentials, - region: isEdge ? Globals.defaultRegion : Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy(), - requestHandler: Globals.getRequestHandler() - }); - } + constructor (credentials: any, endpointType: string) { + const isEdge = endpointType === Globals.endpointTypes.edge; + this.acm = new ACMClient({ + credentials, + region: isEdge ? Globals.defaultRegion : Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy(), + requestHandler: Globals.getRequestHandler() + }); + } - public async getCertArn (domain: DomainConfig): Promise { - let certificateArn; // The arn of the selected certificate - let certificateName = domain.certificateName; // The certificate name + public async getCertArn (domain: DomainConfig): Promise { + let certificateArn; // The arn of the selected certificate + let certificateName = domain.certificateName; // The certificate name - try { - const certificates = await getAWSPagedResults( - this.acm, - "CertificateSummaryList", - "NextToken", - "NextToken", - new ListCertificatesCommand({ CertificateStatuses: certStatuses }) - ); - // enhancement idea: weight the choice of cert so longer expires - // and RenewalEligibility = ELIGIBLE is more preferable - if (certificateName) { - certificateArn = this.getCertArnByCertName(certificates, certificateName); - } else { - certificateName = domain.givenDomainName; - certificateArn = ACMWrapper.getCertArnByDomainName(certificates, certificateName); - } - Logging.logInfo(`Found a certificate ARN: '${certificateArn}'`); - } catch (err) { - throw Error(`Could not search certificates in Certificate Manager.\n${err.message}`); - } - if (certificateArn == null) { - throw Error(`Could not find an in-date certificate for '${certificateName}'.`); + try { + const certificates = await getAWSPagedResults( + this.acm, + "CertificateSummaryList", + "NextToken", + "NextToken", + new ListCertificatesCommand({ CertificateStatuses: certStatuses }) + ); + // enhancement idea: weight the choice of cert so longer expires + // and RenewalEligibility = ELIGIBLE is more preferable + if (certificateName) { + certificateArn = this.getCertArnByCertName(certificates, certificateName); + } else { + certificateName = domain.givenDomainName; + certificateArn = ACMWrapper.getCertArnByDomainName(certificates, certificateName); } - return certificateArn; + Logging.logInfo(`Found a certificate ARN: '${certificateArn}'`); + } catch (err) { + throw Error(`Could not search certificates in Certificate Manager.\n${err.message}`); } + if (certificateArn == null) { + throw Error(`Could not find an in-date certificate for '${certificateName}'.`); + } + return certificateArn; + } - private getCertArnByCertName (certificates, certName): string { - const found = certificates.find((c) => c.DomainName === certName); - if (found) { - return found.CertificateArn; - } - return null; + private getCertArnByCertName (certificates, certName): string { + const found = certificates.find((c) => c.DomainName === certName); + if (found) { + return found.CertificateArn; } + return null; + } - private static getCertArnByDomainName (certificates, domainName): string { - // The more specific name will be the longest - let nameLength = 0; - let certificateArn; - for (const currCert of certificates) { - const allDomainsForCert = [ - currCert.DomainName, - ...(currCert.SubjectAlternativeNameSummaries || []) - ]; - for (const currCertDomain of allDomainsForCert) { - let certificateListName = currCertDomain; - // Looks for wild card and take it out when checking - if (certificateListName[0] === "*") { - certificateListName = certificateListName.substring(1); - } - // Looks to see if the name in the list is within the given domain - // Also checks if the name is more specific than previous ones - if (domainName.includes(certificateListName) && certificateListName.length > nameLength) { - nameLength = certificateListName.length; - certificateArn = currCert.CertificateArn; - } + private static getCertArnByDomainName (certificates, domainName): string { + // The more specific name will be the longest + let nameLength = 0; + let certificateArn; + for (const currCert of certificates) { + const allDomainsForCert = [ + currCert.DomainName, + ...(currCert.SubjectAlternativeNameSummaries || []) + ]; + for (const currCertDomain of allDomainsForCert) { + let certificateListName = currCertDomain; + // Looks for wild card and take it out when checking + if (certificateListName[0] === "*") { + certificateListName = certificateListName.substring(1); + } + // Looks to see if the name in the list is within the given domain + // Also checks if the name is more specific than previous ones + if (domainName.includes(certificateListName) && certificateListName.length > nameLength) { + nameLength = certificateListName.length; + certificateArn = currCert.CertificateArn; } } - return certificateArn; } + return certificateArn; + } } export = ACMWrapper; diff --git a/src/aws/api-gateway-v1-wrapper.ts b/src/aws/api-gateway-v1-wrapper.ts index b4f86fdf..b43d49f6 100644 --- a/src/aws/api-gateway-v1-wrapper.ts +++ b/src/aws/api-gateway-v1-wrapper.ts @@ -25,168 +25,166 @@ import Logging from "../logging"; import { getAWSPagedResults } from "../utils"; class APIGatewayV1Wrapper extends APIGatewayBase { - public readonly apiGateway: APIGatewayClient; + public readonly apiGateway: APIGatewayClient; - constructor (credentials?: any) { - super(); - this.apiGateway = new APIGatewayClient({ - credentials, - region: Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy(), - requestHandler: Globals.getRequestHandler() - }); - } + constructor (credentials?: any) { + super(); + this.apiGateway = new APIGatewayClient({ + credentials, + region: Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy(), + requestHandler: Globals.getRequestHandler() + }); + } - public async createCustomDomain (domain: DomainConfig): Promise { - const providerTags = { - ...Globals.serverless.service.provider.stackTags, - ...Globals.serverless.service.provider.tags - }; + public async createCustomDomain (domain: DomainConfig): Promise { + const providerTags = { + ...Globals.serverless.service.provider.stackTags, + ...Globals.serverless.service.provider.tags + }; - const params: any = { - domainName: domain.givenDomainName, - endpointConfiguration: { - types: [domain.endpointType] - }, - securityPolicy: domain.securityPolicy, - tags: providerTags - }; + const params: any = { + domainName: domain.givenDomainName, + endpointConfiguration: { + types: [domain.endpointType] + }, + securityPolicy: domain.securityPolicy, + tags: providerTags + }; - const isEdgeType = domain.endpointType === Globals.endpointTypes.edge; - if (isEdgeType) { - params.certificateArn = domain.certificateArn; - } else { - params.regionalCertificateArn = domain.certificateArn; + const isEdgeType = domain.endpointType === Globals.endpointTypes.edge; + if (isEdgeType) { + params.certificateArn = domain.certificateArn; + } else { + params.regionalCertificateArn = domain.certificateArn; - if (domain.tlsTruststoreUri) { - params.mutualTlsAuthentication = { - truststoreUri: domain.tlsTruststoreUri - }; + if (domain.tlsTruststoreUri) { + params.mutualTlsAuthentication = { + truststoreUri: domain.tlsTruststoreUri + }; - if (domain.tlsTruststoreVersion) { - params.mutualTlsAuthentication.truststoreVersion = domain.tlsTruststoreVersion; - } + if (domain.tlsTruststoreVersion) { + params.mutualTlsAuthentication.truststoreVersion = domain.tlsTruststoreVersion; } } + } - try { - const domainInfo: CreateDomainNameCommandOutput = await this.apiGateway.send( - new CreateDomainNameCommand(params) - ); - return new DomainInfo(domainInfo); - } catch (err) { - throw new Error( - `V1 - Failed to create custom domain '${domain.givenDomainName}':\n${err.message}` - ); - } + try { + const domainInfo: CreateDomainNameCommandOutput = await this.apiGateway.send( + new CreateDomainNameCommand(params) + ); + return new DomainInfo(domainInfo); + } catch (err) { + throw new Error( + `V1 - Failed to create custom domain '${domain.givenDomainName}':\n${err.message}` + ); } + } - public async getCustomDomain (domain: DomainConfig): Promise { - // Make API call - try { - const domainInfo: GetDomainNameCommandOutput = await this.apiGateway.send( - new GetDomainNameCommand({ - domainName: domain.givenDomainName - }) + public async getCustomDomain (domain: DomainConfig): Promise { + // Make API call + try { + const domainInfo: GetDomainNameCommandOutput = await this.apiGateway.send( + new GetDomainNameCommand({ + domainName: domain.givenDomainName + }) + ); + return new DomainInfo(domainInfo); + } catch (err) { + if (!err.$metadata || err.$metadata.httpStatusCode !== 404) { + throw new Error( + `V1 - Unable to fetch information about '${domain.givenDomainName}':\n${err.message}` ); - return new DomainInfo(domainInfo); - } catch (err) { - if (!err.$metadata || err.$metadata.httpStatusCode !== 404) { - throw new Error( - `V1 - Unable to fetch information about '${domain.givenDomainName}':\n${err.message}` - ); - } - Logging.logWarning(`V1 - '${domain.givenDomainName}' does not exist.`); } + Logging.logWarning(`V1 - '${domain.givenDomainName}' does not exist.`); } + } - public async deleteCustomDomain (domain: DomainConfig): Promise { - // Make API call - try { - await this.apiGateway.send(new DeleteDomainNameCommand({ - domainName: domain.givenDomainName - })); - } catch (err) { - throw new Error(`V1 - Failed to delete custom domain '${domain.givenDomainName}':\n${err.message}`); - } + public async deleteCustomDomain (domain: DomainConfig): Promise { + // Make API call + try { + await this.apiGateway.send(new DeleteDomainNameCommand({ + domainName: domain.givenDomainName + })); + } catch (err) { + throw new Error(`V1 - Failed to delete custom domain '${domain.givenDomainName}':\n${err.message}`); } + } - public async createBasePathMapping (domain: DomainConfig): Promise { - try { - await this.apiGateway.send(new CreateBasePathMappingCommand({ - basePath: domain.basePath, - domainName: domain.givenDomainName, - restApiId: domain.apiId, - stage: domain.stage - })); - Logging.logInfo(`V1 - Created API mapping '${domain.basePath}' for '${domain.givenDomainName}'`); - } catch (err) { - throw new Error( - `V1 - Make sure the '${domain.givenDomainName}' exists. + public async createBasePathMapping (domain: DomainConfig): Promise { + try { + await this.apiGateway.send(new CreateBasePathMappingCommand({ + basePath: domain.basePath, + domainName: domain.givenDomainName, + restApiId: domain.apiId, + stage: domain.stage + })); + Logging.logInfo(`V1 - Created API mapping '${domain.basePath}' for '${domain.givenDomainName}'`); + } catch (err) { + throw new Error( + `V1 - Make sure the '${domain.givenDomainName}' exists. Unable to create base path mapping for '${domain.givenDomainName}':\n${err.message}` - ); - } + ); } + } - public async getBasePathMappings (domain: DomainConfig): Promise { - try { - const items = await getAWSPagedResults( - this.apiGateway, - "items", - "position", - "position", - new GetBasePathMappingsCommand({ - domainName: domain.givenDomainName - }) - ); - return items.map((item) => { - return new ApiGatewayMap(item.restApiId, item.basePath, item.stage, null); - } - ); - } catch (err) { - throw new Error( - `V1 - Make sure the '${domain.givenDomainName}' exists. + public async getBasePathMappings (domain: DomainConfig): Promise { + try { + const items = await getAWSPagedResults( + this.apiGateway, + "items", + "position", + "position", + new GetBasePathMappingsCommand({ + domainName: domain.givenDomainName + }) + ); + return items.map((item) => { + return new ApiGatewayMap(item.restApiId, item.basePath, item.stage, null); + }); + } catch (err) { + throw new Error( + `V1 - Make sure the '${domain.givenDomainName}' exists. Unable to get Base Path Mappings:\n${err.message}` - ); - } + ); } + } - public async updateBasePathMapping (domain: DomainConfig): Promise { - Logging.logInfo(`V1 - Updating API mapping from '${domain.apiMapping.basePath}' + public async updateBasePathMapping (domain: DomainConfig): Promise { + Logging.logInfo(`V1 - Updating API mapping from '${domain.apiMapping.basePath}' to '${domain.basePath}' for '${domain.givenDomainName}'`); - try { - await this.apiGateway.send(new UpdateBasePathMappingCommand({ - basePath: domain.apiMapping.basePath, - domainName: domain.givenDomainName, - patchOperations: [{ - op: "replace", - path: "/basePath", - value: domain.basePath - }] - } - )); - } catch (err) { - throw new Error( - `V1 - Unable to update base path mapping for '${domain.givenDomainName}':\n${err.message}` - ); - } + try { + await this.apiGateway.send(new UpdateBasePathMappingCommand({ + basePath: domain.apiMapping.basePath, + domainName: domain.givenDomainName, + patchOperations: [{ + op: "replace", + path: "/basePath", + value: domain.basePath + }] + })); + } catch (err) { + throw new Error( + `V1 - Unable to update base path mapping for '${domain.givenDomainName}':\n${err.message}` + ); } + } - public async deleteBasePathMapping (domain: DomainConfig): Promise { - try { - await this.apiGateway.send( - new DeleteBasePathMappingCommand({ - basePath: domain.apiMapping.basePath, - domainName: domain.givenDomainName - }) - ); - Logging.logInfo(`V1 - Removed '${domain.apiMapping.basePath}' base path mapping`); - } catch (err) { - throw new Error( - `V1 - Unable to remove base path mapping for '${domain.givenDomainName}':\n${err.message}` - ); - } + public async deleteBasePathMapping (domain: DomainConfig): Promise { + try { + await this.apiGateway.send( + new DeleteBasePathMappingCommand({ + basePath: domain.apiMapping.basePath, + domainName: domain.givenDomainName + }) + ); + Logging.logInfo(`V1 - Removed '${domain.apiMapping.basePath}' base path mapping`); + } catch (err) { + throw new Error( + `V1 - Unable to remove base path mapping for '${domain.givenDomainName}':\n${err.message}` + ); } + } } export = APIGatewayV1Wrapper; diff --git a/src/aws/api-gateway-v2-wrapper.ts b/src/aws/api-gateway-v2-wrapper.ts index fa761b64..7dc58bf9 100644 --- a/src/aws/api-gateway-v2-wrapper.ts +++ b/src/aws/api-gateway-v2-wrapper.ts @@ -25,197 +25,197 @@ import Logging from "../logging"; import { getAWSPagedResults } from "../utils"; class APIGatewayV2Wrapper extends APIGatewayBase { - public readonly apiGateway: ApiGatewayV2Client; + public readonly apiGateway: ApiGatewayV2Client; - constructor (credentials?: any) { - super(); - this.apiGateway = new ApiGatewayV2Client({ - credentials, - region: Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy(), - requestHandler: Globals.getRequestHandler() - }); - } + constructor (credentials?: any) { + super(); + this.apiGateway = new ApiGatewayV2Client({ + credentials, + region: Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy(), + requestHandler: Globals.getRequestHandler() + }); + } - /** - * Creates Custom Domain Name - * @param domain: DomainConfig - */ - public async createCustomDomain (domain: DomainConfig): Promise { - const providerTags = { - ...Globals.serverless.service.provider.stackTags, - ...Globals.serverless.service.provider.tags - }; + /** + * Creates Custom Domain Name + * @param domain: DomainConfig + */ + public async createCustomDomain (domain: DomainConfig): Promise { + const providerTags = { + ...Globals.serverless.service.provider.stackTags, + ...Globals.serverless.service.provider.tags + }; - const params: any = { - DomainName: domain.givenDomainName, - DomainNameConfigurations: [{ - CertificateArn: domain.certificateArn, - EndpointType: domain.endpointType, - SecurityPolicy: domain.securityPolicy - }], - Tags: providerTags - }; + const params: any = { + DomainName: domain.givenDomainName, + DomainNameConfigurations: [{ + CertificateArn: domain.certificateArn, + EndpointType: domain.endpointType, + SecurityPolicy: domain.securityPolicy + }], + Tags: providerTags + }; - const isEdgeType = domain.endpointType === Globals.endpointTypes.edge; - if (!isEdgeType && domain.tlsTruststoreUri) { - params.MutualTlsAuthentication = { - TruststoreUri: domain.tlsTruststoreUri - }; - - if (domain.tlsTruststoreVersion) { - params.MutualTlsAuthentication.TruststoreVersion = domain.tlsTruststoreVersion; - } - } + const isEdgeType = domain.endpointType === Globals.endpointTypes.edge; + if (!isEdgeType && domain.tlsTruststoreUri) { + params.MutualTlsAuthentication = { + TruststoreUri: domain.tlsTruststoreUri + }; - try { - const domainInfo: CreateDomainNameCommandOutput = await this.apiGateway.send( - new CreateDomainNameCommand(params) - ); - return new DomainInfo(domainInfo); - } catch (err) { - throw new Error( - `V2 - Failed to create custom domain '${domain.givenDomainName}':\n${err.message}` - ); + if (domain.tlsTruststoreVersion) { + params.MutualTlsAuthentication.TruststoreVersion = domain.tlsTruststoreVersion; } } - /** - * Get Custom Domain Info - * @param domain: DomainConfig - */ - public async getCustomDomain (domain: DomainConfig): Promise { - // Make API call - try { - const domainInfo: GetDomainNameCommandOutput = await this.apiGateway.send( - new GetDomainNameCommand({ - DomainName: domain.givenDomainName - }) - ); - return new DomainInfo(domainInfo); - } catch (err) { - if (!err.$metadata || err.$metadata.httpStatusCode !== 404) { - throw new Error( - `V2 - Unable to fetch information about '${domain.givenDomainName}':\n${err.message}` - ); - } - Logging.logInfo(`V2 - '${domain.givenDomainName}' does not exist.`); - } + try { + const domainInfo: CreateDomainNameCommandOutput = await this.apiGateway.send( + new CreateDomainNameCommand(params) + ); + return new DomainInfo(domainInfo); + } catch (err) { + throw new Error( + `V2 - Failed to create custom domain '${domain.givenDomainName}':\n${err.message}` + ); } + } - /** - * Delete Custom Domain Name - * @param domain: DomainConfig - */ - public async deleteCustomDomain (domain: DomainConfig): Promise { - // Make API call - try { - await this.apiGateway.send( - new DeleteDomainNameCommand({ - DomainName: domain.givenDomainName - }) - ); - } catch (err) { + /** + * Get Custom Domain Info + * @param domain: DomainConfig + */ + public async getCustomDomain (domain: DomainConfig): Promise { + // Make API call + try { + const domainInfo: GetDomainNameCommandOutput = await this.apiGateway.send( + new GetDomainNameCommand({ + DomainName: domain.givenDomainName + }) + ); + return new DomainInfo(domainInfo); + } catch (err) { + if (!err.$metadata || err.$metadata.httpStatusCode !== 404) { throw new Error( - `V2 - Failed to delete custom domain '${domain.givenDomainName}':\n${err.message}` + `V2 - Unable to fetch information about '${domain.givenDomainName}':\n${err.message}` ); } + Logging.logInfo(`V2 - '${domain.givenDomainName}' does not exist.`); } + } - /** - * Create Base Path Mapping - * @param domain: DomainConfig - */ - public async createBasePathMapping (domain: DomainConfig): Promise { - if (domain.apiType === Globals.apiTypes.http && domain.stage !== Globals.defaultStage) { - Logging.logWarning( - `Using a HTTP API with a stage name other than '${Globals.defaultStage}'. ` + - `HTTP APIs require a stage named '${Globals.defaultStage}'. ` + - "Please make sure that stage exists in the API Gateway. " + - "See https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-stages.html" - ); - } - try { - await this.apiGateway.send( - new CreateApiMappingCommand({ - ApiId: domain.apiId, - ApiMappingKey: domain.basePath, - DomainName: domain.givenDomainName, - Stage: domain.stage - }) - ); - Logging.logInfo(`V2 - Created API mapping '${domain.basePath}' for '${domain.givenDomainName}'`); - } catch (err) { - throw new Error( - `V2 - Unable to create base path mapping for '${domain.givenDomainName}':\n${err.message}` - ); - } + /** + * Delete Custom Domain Name + * @param domain: DomainConfig + */ + public async deleteCustomDomain (domain: DomainConfig): Promise { + // Make API call + try { + await this.apiGateway.send( + new DeleteDomainNameCommand({ + DomainName: domain.givenDomainName + }) + ); + } catch (err) { + throw new Error( + `V2 - Failed to delete custom domain '${domain.givenDomainName}':\n${err.message}` + ); } + } - /** - * Get APi Mapping - * @param domain: DomainConfig - */ - public async getBasePathMappings (domain: DomainConfig): Promise { - try { - const items = await getAWSPagedResults( - this.apiGateway, - "Items", - "NextToken", - "NextToken", - new GetApiMappingsCommand({ - DomainName: domain.givenDomainName - }) - ); - return items.map( - (item) => new ApiGatewayMap(item.ApiId, item.ApiMappingKey, item.Stage, item.ApiMappingId) - ); - } catch (err) { - throw new Error( - `V2 - Make sure the '${domain.givenDomainName}' exists. Unable to get API Mappings:\n${err.message}` - ); - } + /** + * Create Base Path Mapping + * @param domain: DomainConfig + */ + public async createBasePathMapping (domain: DomainConfig): Promise { + if (domain.apiType === Globals.apiTypes.http && domain.stage !== Globals.defaultStage) { + Logging.logWarning( + `Using a HTTP API with a stage name other than '${Globals.defaultStage}'. ` + + `HTTP APIs require a stage named '${Globals.defaultStage}'. ` + + "Please make sure that stage exists in the API Gateway. " + + "See https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-stages.html" + ); + } + try { + await this.apiGateway.send( + new CreateApiMappingCommand({ + ApiId: domain.apiId, + ApiMappingKey: domain.basePath, + DomainName: domain.givenDomainName, + Stage: domain.stage + }) + ); + Logging.logInfo(`V2 - Created API mapping '${domain.basePath}' for '${domain.givenDomainName}'`); + } catch (err) { + throw new Error( + `V2 - Unable to create base path mapping for '${domain.givenDomainName}':\n${err.message}` + ); } + } - /** - * Update APi Mapping - * @param domain: DomainConfig - */ - public async updateBasePathMapping (domain: DomainConfig): Promise { - try { - await this.apiGateway.send( - new UpdateApiMappingCommand({ - ApiId: domain.apiId, - ApiMappingId: domain.apiMapping.apiMappingId, - ApiMappingKey: domain.basePath, - DomainName: domain.givenDomainName, - Stage: domain.stage - }) - ); - Logging.logInfo(`V2 - Updated API mapping to '${domain.basePath}' for '${domain.givenDomainName}'`); - } catch (err) { - throw new Error( - `V2 - Unable to update base path mapping for '${domain.givenDomainName}':\n${err.message}` - ); - } + /** + * Get APi Mapping + * @param domain: DomainConfig + */ + public async getBasePathMappings (domain: DomainConfig): Promise { + try { + const items = await getAWSPagedResults( + this.apiGateway, + "Items", + "NextToken", + "NextToken", + new GetApiMappingsCommand({ + DomainName: domain.givenDomainName + }) + ); + return items.map( + (item) => new ApiGatewayMap(item.ApiId, item.ApiMappingKey, item.Stage, item.ApiMappingId) + ); + } catch (err) { + throw new Error( + `V2 - Make sure the '${domain.givenDomainName}' exists. Unable to get API Mappings:\n${err.message}` + ); } + } - /** - * Delete Api Mapping - */ - public async deleteBasePathMapping (domain: DomainConfig): Promise { - try { - await this.apiGateway.send(new DeleteApiMappingCommand({ + /** + * Update APi Mapping + * @param domain: DomainConfig + */ + public async updateBasePathMapping (domain: DomainConfig): Promise { + try { + await this.apiGateway.send( + new UpdateApiMappingCommand({ + ApiId: domain.apiId, ApiMappingId: domain.apiMapping.apiMappingId, - DomainName: domain.givenDomainName - })); - Logging.logInfo(`V2 - Removed API Mapping with id: '${domain.apiMapping.apiMappingId}'`); - } catch (err) { - throw new Error( - `V2 - Unable to remove base path mapping for '${domain.givenDomainName}':\n${err.message}` - ); - } + ApiMappingKey: domain.basePath, + DomainName: domain.givenDomainName, + Stage: domain.stage + }) + ); + Logging.logInfo(`V2 - Updated API mapping to '${domain.basePath}' for '${domain.givenDomainName}'`); + } catch (err) { + throw new Error( + `V2 - Unable to update base path mapping for '${domain.givenDomainName}':\n${err.message}` + ); + } + } + + /** + * Delete Api Mapping + */ + public async deleteBasePathMapping (domain: DomainConfig): Promise { + try { + await this.apiGateway.send(new DeleteApiMappingCommand({ + ApiMappingId: domain.apiMapping.apiMappingId, + DomainName: domain.givenDomainName + })); + Logging.logInfo(`V2 - Removed API Mapping with id: '${domain.apiMapping.apiMappingId}'`); + } catch (err) { + throw new Error( + `V2 - Unable to remove base path mapping for '${domain.givenDomainName}':\n${err.message}` + ); } + } } export = APIGatewayV2Wrapper; diff --git a/src/aws/cloud-formation-wrapper.ts b/src/aws/cloud-formation-wrapper.ts index 57292a49..ccbafa8c 100644 --- a/src/aws/cloud-formation-wrapper.ts +++ b/src/aws/cloud-formation-wrapper.ts @@ -20,181 +20,181 @@ import Logging from "../logging"; import { getAWSPagedResults } from "../utils"; class CloudFormationWrapper { - public cloudFormation: CloudFormationClient; - public stackName: string; - - constructor (credentials?: any) { - // for the CloudFormation stack we should use the `base` stage not the plugin custom stage - const defaultStackName = Globals.serverless.service.service + "-" + Globals.getBaseStage(); - this.stackName = Globals.serverless.service.provider.stackName || defaultStackName; - this.cloudFormation = new CloudFormationClient({ - credentials, - region: Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy(), - requestHandler: Globals.getRequestHandler() - }); + public cloudFormation: CloudFormationClient; + public stackName: string; + + constructor (credentials?: any) { + // for the CloudFormation stack we should use the `base` stage not the plugin custom stage + const defaultStackName = Globals.serverless.service.service + "-" + Globals.getBaseStage(); + this.stackName = Globals.serverless.service.provider.stackName || defaultStackName; + this.cloudFormation = new CloudFormationClient({ + credentials, + region: Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy(), + requestHandler: Globals.getRequestHandler() + }); + } + + /** + * Get an API id from the existing config or CloudFormation stack resources or outputs + */ + public async findApiId (apiType: string): Promise { + const configApiId = await this.getConfigId(apiType); + if (configApiId) { + return configApiId; } - /** - * Get an API id from the existing config or CloudFormation stack resources or outputs - */ - public async findApiId (apiType: string): Promise { - const configApiId = await this.getConfigId(apiType); - if (configApiId) { - return configApiId; - } - - return await this.getStackApiId(apiType); - } + return await this.getStackApiId(apiType); + } - /** - * Get an API id from the existing config or CloudFormation stack based on provider.apiGateway params - */ - private async getConfigId (apiType: string): Promise { - const apiGateway = Globals.serverless.service.provider.apiGateway || {}; - const apiIdKey = Globals.gatewayAPIIdKeys[apiType]; - const apiGatewayValue = apiGateway[apiIdKey]; - - if (apiGatewayValue) { - if (typeof apiGatewayValue === "string") { - return apiGatewayValue; - } + /** + * Get an API id from the existing config or CloudFormation stack based on provider.apiGateway params + */ + private async getConfigId (apiType: string): Promise { + const apiGateway = Globals.serverless.service.provider.apiGateway || {}; + const apiIdKey = Globals.gatewayAPIIdKeys[apiType]; + const apiGatewayValue = apiGateway[apiIdKey]; - return await this.getCloudformationId(apiGatewayValue, apiType); + if (apiGatewayValue) { + if (typeof apiGatewayValue === "string") { + return apiGatewayValue; } - return null; + return await this.getCloudformationId(apiGatewayValue, apiType); } - private async getCloudformationId (apiGatewayValue: object, apiType: string): Promise { - // in case object and Fn::ImportValue try to get API id from the CloudFormation outputs - const importName = apiGatewayValue[Globals.CFFuncNames.fnImport]; - if (importName) { - const importValues = await this.getImportValues([importName]); - const nameValue = importValues[importName]; - if (!nameValue) { - Logging.logWarning(`CloudFormation ImportValue '${importName}' not found in the outputs`); - } - return nameValue; - } - - const ref = apiGatewayValue[Globals.CFFuncNames.ref]; - if (ref) { - try { - return await this.getStackApiId(apiType, ref); - } catch (error) { - Logging.logWarning(`Unable to get ref ${ref} value.\n ${error.message}`); - return null; - } + return null; + } + + private async getCloudformationId (apiGatewayValue: object, apiType: string): Promise { + // in case object and Fn::ImportValue try to get API id from the CloudFormation outputs + const importName = apiGatewayValue[Globals.CFFuncNames.fnImport]; + if (importName) { + const importValues = await this.getImportValues([importName]); + const nameValue = importValues[importName]; + if (!nameValue) { + Logging.logWarning(`CloudFormation ImportValue '${importName}' not found in the outputs`); } - - // log warning not supported restApiId - Logging.logWarning(`Unsupported apiGateway.${apiType} object`); - - return null; + return nameValue; } - /** - * Gets rest API id from CloudFormation stack or nested stack - */ - public async getStackApiId (apiType: string, logicalResourceId: string = null): Promise { - if (!logicalResourceId) { - logicalResourceId = Globals.CFResourceIds[apiType]; - } - - let response; + const ref = apiGatewayValue[Globals.CFFuncNames.ref]; + if (ref) { try { - // trying to get information for specified stack name - response = await this.getStack(logicalResourceId, this.stackName); - } catch { - // in case error trying to get information from some of nested stacks - response = await this.getNestedStack(logicalResourceId, this.stackName); + return await this.getStackApiId(apiType, ref); + } catch (error) { + Logging.logWarning(`Unable to get ref ${ref} value.\n ${error.message}`); + return null; } + } - if (!response) { - throw new Error(`Failed to find a stack ${this.stackName}\n`); - } + // log warning not supported restApiId + Logging.logWarning(`Unsupported apiGateway.${apiType} object`); - const apiId = response.StackResourceDetail.PhysicalResourceId; - if (!apiId) { - throw new Error(`No ApiId associated with CloudFormation stack ${this.stackName}`); - } + return null; + } - return apiId; + /** + * Gets rest API id from CloudFormation stack or nested stack + */ + public async getStackApiId (apiType: string, logicalResourceId: string = null): Promise { + if (!logicalResourceId) { + logicalResourceId = Globals.CFResourceIds[apiType]; } - /** - * Gets values by names from cloudformation exports - */ - public async getImportValues (names: string[]): Promise { - const exports = await getAWSPagedResults( - this.cloudFormation, - "Exports", - "NextToken", - "NextToken", - new ListExportsCommand({}) - ); - // filter Exports by names which we need - const filteredExports = exports.filter((item) => names.indexOf(item.Name) !== -1); - // converting a list of unique values to dict - // [{Name: "export-name", Value: "export-value"}, ...] - > {"export-name": "export-value"} - return filteredExports.reduce((prev, current) => ({ ...prev, [current.Name]: current.Value }), {}); + let response; + try { + // trying to get information for specified stack name + response = await this.getStack(logicalResourceId, this.stackName); + } catch { + // in case error trying to get information from some of nested stacks + response = await this.getNestedStack(logicalResourceId, this.stackName); } - /** - * Returns a description of the specified resource in the specified stack. - */ - public async getStack (logicalResourceId: string, stackName: string): Promise { - try { - return await this.cloudFormation.send( - new DescribeStackResourceCommand({ - LogicalResourceId: logicalResourceId, - StackName: stackName - }) - ); - } catch (err) { - throw new Error(`Failed to find CloudFormation resources with an error: ${err.message}\n`); - } + if (!response) { + throw new Error(`Failed to find a stack ${this.stackName}\n`); } - /** - * Returns a description of the specified resource in the specified nested stack. - */ - public async getNestedStack (logicalResourceId: string, stackName: string) { - // get all stacks from the CloudFormation - const stacks = await getAWSPagedResults( - this.cloudFormation, - "Stacks", - "NextToken", - "NextToken", - new DescribeStacksCommand({}) - ); + const apiId = response.StackResourceDetail.PhysicalResourceId; + if (!apiId) { + throw new Error(`No ApiId associated with CloudFormation stack ${this.stackName}`); + } - // filter stacks by given stackName and check by nested stack RootId - const regex = new RegExp("/" + stackName + "/"); - const filteredStackNames = stacks - .reduce((acc, stack) => { - if (!stack.RootId) { - return acc; - } - const match = stack.RootId.match(regex); - if (match) { - acc.push(stack.StackName); - } + return apiId; + } + + /** + * Gets values by names from cloudformation exports + */ + public async getImportValues (names: string[]): Promise { + const exports = await getAWSPagedResults( + this.cloudFormation, + "Exports", + "NextToken", + "NextToken", + new ListExportsCommand({}) + ); + // filter Exports by names which we need + const filteredExports = exports.filter((item) => names.indexOf(item.Name) !== -1); + // converting a list of unique values to dict + // [{Name: "export-name", Value: "export-value"}, ...] - > {"export-name": "export-value"} + return filteredExports.reduce((prev, current) => ({ ...prev, [current.Name]: current.Value }), {}); + } + + /** + * Returns a description of the specified resource in the specified stack. + */ + public async getStack (logicalResourceId: string, stackName: string): Promise { + try { + return await this.cloudFormation.send( + new DescribeStackResourceCommand({ + LogicalResourceId: logicalResourceId, + StackName: stackName + }) + ); + } catch (err) { + throw new Error(`Failed to find CloudFormation resources with an error: ${err.message}\n`); + } + } + + /** + * Returns a description of the specified resource in the specified nested stack. + */ + public async getNestedStack (logicalResourceId: string, stackName: string) { + // get all stacks from the CloudFormation + const stacks = await getAWSPagedResults( + this.cloudFormation, + "Stacks", + "NextToken", + "NextToken", + new DescribeStacksCommand({}) + ); + + // filter stacks by given stackName and check by nested stack RootId + const regex = new RegExp("/" + stackName + "/"); + const filteredStackNames = stacks + .reduce((acc, stack) => { + if (!stack.RootId) { return acc; - }, []); - - for (const name of filteredStackNames) { - try { - // stop the loop and return the stack details in case the first one found - // in case of error continue the looping - return await this.getStack(logicalResourceId, name); - } catch (err) { - Logging.logWarning(err.message); } + const match = stack.RootId.match(regex); + if (match) { + acc.push(stack.StackName); + } + return acc; + }, []); + + for (const name of filteredStackNames) { + try { + // stop the loop and return the stack details in case the first one found + // in case of error continue the looping + return await this.getStack(logicalResourceId, name); + } catch (err) { + Logging.logWarning(err.message); } - return null; } + return null; + } } export = CloudFormationWrapper; diff --git a/src/aws/route53-wrapper.ts b/src/aws/route53-wrapper.ts index be105e5c..0807a0bb 100644 --- a/src/aws/route53-wrapper.ts +++ b/src/aws/route53-wrapper.ts @@ -12,159 +12,159 @@ import { import { getAWSPagedResults } from "../utils"; class Route53Wrapper { - public route53: Route53Client; - private readonly region: string; + public route53: Route53Client; + private readonly region: string; - constructor (credentials?: any, region?: string) { - // not null and not undefined - if (credentials) { - this.region = region || Globals.getRegion(); - this.route53 = new Route53Client({ - credentials, - region: this.region, - retryStrategy: Globals.getRetryStrategy(), - requestHandler: Globals.getRequestHandler() - }); - } else { - this.region = Globals.getRegion(); - this.route53 = new Route53Client({ - region: this.region, - retryStrategy: Globals.getRetryStrategy(), - requestHandler: Globals.getRequestHandler() - }); - } + constructor (credentials?: any, region?: string) { + // not null and not undefined + if (credentials) { + this.region = region || Globals.getRegion(); + this.route53 = new Route53Client({ + credentials, + region: this.region, + retryStrategy: Globals.getRetryStrategy(), + requestHandler: Globals.getRequestHandler() + }); + } else { + this.region = Globals.getRegion(); + this.route53 = new Route53Client({ + region: this.region, + retryStrategy: Globals.getRetryStrategy(), + requestHandler: Globals.getRequestHandler() + }); } + } - /** - * Gets Route53 HostedZoneId from user or from AWS - */ - public async getRoute53HostedZoneId (domain: DomainConfig, isHostedZonePrivate?: boolean): Promise { - if (domain.hostedZoneId) { - Logging.logInfo(`Selected specific hostedZoneId ${domain.hostedZoneId}`); - return domain.hostedZoneId; - } + /** + * Gets Route53 HostedZoneId from user or from AWS + */ + public async getRoute53HostedZoneId (domain: DomainConfig, isHostedZonePrivate?: boolean): Promise { + if (domain.hostedZoneId) { + Logging.logInfo(`Selected specific hostedZoneId ${domain.hostedZoneId}`); + return domain.hostedZoneId; + } - const isPrivateDefined = typeof isHostedZonePrivate !== "undefined"; - if (isPrivateDefined) { - const zoneTypeString = isHostedZonePrivate ? "private" : "public"; - Logging.logInfo(`Filtering to only ${zoneTypeString} zones.`); - } + const isPrivateDefined = typeof isHostedZonePrivate !== "undefined"; + if (isPrivateDefined) { + const zoneTypeString = isHostedZonePrivate ? "private" : "public"; + Logging.logInfo(`Filtering to only ${zoneTypeString} zones.`); + } - let hostedZones = []; - try { - hostedZones = await getAWSPagedResults( - this.route53, - "HostedZones", - "Marker", - "NextMarker", - new ListHostedZonesCommand({}) - ); - Logging.logInfo(`Founded hosted zones list: ${hostedZones.map((zone) => zone.Name)}.`); - } catch (err) { - throw new Error(`Unable to list hosted zones in Route53.\n${err.message}`); - } + let hostedZones = []; + try { + hostedZones = await getAWSPagedResults( + this.route53, + "HostedZones", + "Marker", + "NextMarker", + new ListHostedZonesCommand({}) + ); + Logging.logInfo(`Founded hosted zones list: ${hostedZones.map((zone) => zone.Name)}.`); + } catch (err) { + throw new Error(`Unable to list hosted zones in Route53.\n${err.message}`); + } - // removing the first part of the domain name, api.test.com => test.com - const domainNameHost = domain.givenDomainName.substring(domain.givenDomainName.indexOf(".") + 1); - const targetHostedZone = hostedZones - .filter((hostedZone) => { - return !isPrivateDefined || isHostedZonePrivate === hostedZone.Config.PrivateZone; - }) - .filter((hostedZone) => { - const hostedZoneName = hostedZone.Name.replace(/\.$/, ""); - return domain.givenDomainName === hostedZoneName || domainNameHost.endsWith(hostedZoneName); - }) - .sort((zone1, zone2) => zone2.Name.length - zone1.Name.length) - .shift(); + // removing the first part of the domain name, api.test.com => test.com + const domainNameHost = domain.givenDomainName.substring(domain.givenDomainName.indexOf(".") + 1); + const targetHostedZone = hostedZones + .filter((hostedZone) => { + return !isPrivateDefined || isHostedZonePrivate === hostedZone.Config.PrivateZone; + }) + .filter((hostedZone) => { + const hostedZoneName = hostedZone.Name.replace(/\.$/, ""); + return domain.givenDomainName === hostedZoneName || domainNameHost.endsWith(hostedZoneName); + }) + .sort((zone1, zone2) => zone2.Name.length - zone1.Name.length) + .shift(); - if (targetHostedZone) { - return targetHostedZone.Id.replace("/hostedzone/", ""); - } else { - throw new Error(`Could not find hosted zone '${domain.givenDomainName}'`); - } + if (targetHostedZone) { + return targetHostedZone.Id.replace("/hostedzone/", ""); + } else { + throw new Error(`Could not find hosted zone '${domain.givenDomainName}'`); } + } - /** - * Change A Alias record through Route53 based on given action - * @param action: String descriptor of change to be made. Valid actions are ['UPSERT', 'DELETE'] - * @param domain: DomainInfo object containing info about custom domain - */ - public async changeResourceRecordSet (action: ChangeAction, domain: DomainConfig): Promise { - if (domain.createRoute53Record === false) { - Logging.logInfo(`Skipping ${action === ChangeAction.DELETE ? "removal" : "creation"} of Route53 record.`); - return; - } - Logging.logInfo(`Creating/updating route53 record for '${domain.givenDomainName}'.`); - // Set up parameters - const route53HostedZoneId = await this.getRoute53HostedZoneId(domain, domain.hostedZonePrivate); - const route53Params = domain.route53Params; - const route53healthCheck = route53Params.healthCheckId ? { HealthCheckId: route53Params.healthCheckId } : {}; - const domainInfo = domain.domainInfo ?? { - domainName: domain.givenDomainName, - hostedZoneId: route53HostedZoneId - }; - - let routingOptions = {}; - if (route53Params.routingPolicy === Globals.routingPolicies.latency) { - routingOptions = { - Region: this.region, - SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, - ...route53healthCheck - }; - } + /** + * Change A Alias record through Route53 based on given action + * @param action: String descriptor of change to be made. Valid actions are ['UPSERT', 'DELETE'] + * @param domain: DomainInfo object containing info about custom domain + */ + public async changeResourceRecordSet (action: ChangeAction, domain: DomainConfig): Promise { + if (domain.createRoute53Record === false) { + Logging.logInfo(`Skipping ${action === ChangeAction.DELETE ? "removal" : "creation"} of Route53 record.`); + return; + } + Logging.logInfo(`Creating/updating route53 record for '${domain.givenDomainName}'.`); + // Set up parameters + const route53HostedZoneId = await this.getRoute53HostedZoneId(domain, domain.hostedZonePrivate); + const route53Params = domain.route53Params; + const route53healthCheck = route53Params.healthCheckId ? { HealthCheckId: route53Params.healthCheckId } : {}; + const domainInfo = domain.domainInfo ?? { + domainName: domain.givenDomainName, + hostedZoneId: route53HostedZoneId + }; - if (route53Params.routingPolicy === Globals.routingPolicies.weighted) { - routingOptions = { - Weight: route53Params.weight, - SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, - ...route53healthCheck - }; - } + let routingOptions = {}; + if (route53Params.routingPolicy === Globals.routingPolicies.latency) { + routingOptions = { + Region: this.region, + SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, + ...route53healthCheck + }; + } - let hostedZoneIds: string[]; - if (domain.splitHorizonDns) { - hostedZoneIds = await Promise.all([ - this.getRoute53HostedZoneId(domain, false), - this.getRoute53HostedZoneId(domain, true) - ]); - } else { - hostedZoneIds = [route53HostedZoneId]; - } + if (route53Params.routingPolicy === Globals.routingPolicies.weighted) { + routingOptions = { + Weight: route53Params.weight, + SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, + ...route53healthCheck + }; + } - const recordsToCreate = domain.createRoute53IPv6Record ? [RRType.A, RRType.AAAA] : [RRType.A]; - for (const hostedZoneId of hostedZoneIds) { - const changes = recordsToCreate.map((Type) => ({ - Action: action, - ResourceRecordSet: { - AliasTarget: { - DNSName: domainInfo.domainName, - EvaluateTargetHealth: false, - HostedZoneId: domainInfo.hostedZoneId - }, - Name: domain.givenDomainName, - Type, - ...routingOptions - } - })); + let hostedZoneIds: string[]; + if (domain.splitHorizonDns) { + hostedZoneIds = await Promise.all([ + this.getRoute53HostedZoneId(domain, false), + this.getRoute53HostedZoneId(domain, true) + ]); + } else { + hostedZoneIds = [route53HostedZoneId]; + } - const params = { - ChangeBatch: { - Changes: changes, - Comment: `Record created by "${Globals.pluginName}"` + const recordsToCreate = domain.createRoute53IPv6Record ? [RRType.A, RRType.AAAA] : [RRType.A]; + for (const hostedZoneId of hostedZoneIds) { + const changes = recordsToCreate.map((Type) => ({ + Action: action, + ResourceRecordSet: { + AliasTarget: { + DNSName: domainInfo.domainName, + EvaluateTargetHealth: false, + HostedZoneId: domainInfo.hostedZoneId }, - HostedZoneId: hostedZoneId - }; - // Make API call - try { - await this.route53.send(new ChangeResourceRecordSetsCommand(params)); - } catch (err) { - throw new Error( - `Failed to ${action} ${recordsToCreate.join(",")} Alias for '${domain.givenDomainName}':\n - ${err.message}` - ); + Name: domain.givenDomainName, + Type, + ...routingOptions } + })); + + const params = { + ChangeBatch: { + Changes: changes, + Comment: `Record created by "${Globals.pluginName}"` + }, + HostedZoneId: hostedZoneId + }; + // Make API call + try { + await this.route53.send(new ChangeResourceRecordSetsCommand(params)); + } catch (err) { + throw new Error( + `Failed to ${action} ${recordsToCreate.join(",")} Alias for '${domain.givenDomainName}':\n + ${err.message}` + ); } } + } } export = Route53Wrapper; diff --git a/src/aws/s3-wrapper.ts b/src/aws/s3-wrapper.ts index 11ec7ccb..4ec39023 100644 --- a/src/aws/s3-wrapper.ts +++ b/src/aws/s3-wrapper.ts @@ -4,47 +4,47 @@ import { HeadObjectCommand, HeadObjectRequest, S3Client } from "@aws-sdk/client- import Globals from "../globals"; class S3Wrapper { - public s3: S3Client; - - constructor (credentials?: any) { - this.s3 = new S3Client({ - credentials, - region: Globals.getRegion(), - requestHandler: Globals.getRequestHandler() - }); + public s3: S3Client; + + constructor (credentials?: any) { + this.s3 = new S3Client({ + credentials, + region: Globals.getRegion(), + requestHandler: Globals.getRequestHandler() + }); + } + + /** + * * Checks whether the Mutual TLS certificate exists in S3 or not + */ + public async assertTlsCertObjectExists (domain: DomainConfig): Promise { + const { Bucket, Key } = S3Wrapper.extractBucketAndKey(domain.tlsTruststoreUri); + const params: HeadObjectRequest = { Bucket, Key }; + + if (domain.tlsTruststoreVersion) { + params.VersionId = domain.tlsTruststoreVersion; } - /** - * * Checks whether the Mutual TLS certificate exists in S3 or not - */ - public async assertTlsCertObjectExists (domain: DomainConfig): Promise { - const { Bucket, Key } = S3Wrapper.extractBucketAndKey(domain.tlsTruststoreUri); - const params: HeadObjectRequest = { Bucket, Key }; - - if (domain.tlsTruststoreVersion) { - params.VersionId = domain.tlsTruststoreVersion; + try { + await this.s3.send(new HeadObjectCommand(params)); + } catch (err) { + if (!err.$metadata || err.$metadata.httpStatusCode !== 403) { + throw Error(`Could not head S3 object at ${domain.tlsTruststoreUri}.\n${err.message}`); } - try { - await this.s3.send(new HeadObjectCommand(params)); - } catch (err) { - if (!err.$metadata || err.$metadata.httpStatusCode !== 403) { - throw Error(`Could not head S3 object at ${domain.tlsTruststoreUri}.\n${err.message}`); - } - - Logging.logWarning( - `Forbidden to check the existence of the S3 object ${domain.tlsTruststoreUri} due to\n${err}` - ); - } - } - - /** - * * Extracts Bucket and Key from the given s3 uri - */ - private static extractBucketAndKey (uri: string): { Bucket: string; Key: string } { - const { hostname, pathname } = new URL(uri); - return { Bucket: hostname, Key: pathname.substring(1) }; + Logging.logWarning( + `Forbidden to check the existence of the S3 object ${domain.tlsTruststoreUri} due to\n${err}` + ); } + } + + /** + * * Extracts Bucket and Key from the given s3 uri + */ + private static extractBucketAndKey (uri: string): { Bucket: string; Key: string } { + const { hostname, pathname } = new URL(uri); + return { Bucket: hostname, Key: pathname.substring(1) }; + } } export = S3Wrapper; diff --git a/src/models/domain-config.ts b/src/models/domain-config.ts index 6eacdfd6..be5020e2 100644 --- a/src/models/domain-config.ts +++ b/src/models/domain-config.ts @@ -54,8 +54,8 @@ class DomainConfig { this.preserveExternalPathMappings = evaluateBoolean(config.preserveExternalPathMappings, false); this.basePath = DomainConfig._getBasePath(config.basePath); this.apiType = DomainConfig._getApiType(config.apiType); - // apiType should be defined before stage - this.stage = DomainConfig._getStage(config.stage, this.apiType); + // apiType and basePath should be defined before stage + this.stage = DomainConfig._getStage(config.stage, this.apiType, this.basePath); this.endpointType = DomainConfig._getEndpointType(config.endpointType); this.tlsTruststoreUri = DomainConfig._getTLSTruststoreUri(config.tlsTruststoreUri, this.endpointType); this.tlsTruststoreVersion = config.tlsTruststoreVersion; @@ -64,8 +64,8 @@ class DomainConfig { this.splitHorizonDns = !this.hostedZoneId && !this.hostedZonePrivate && evaluateBoolean(config.splitHorizonDns, false); } - private static _getStage (stage: string, apiType: string) { - if (apiType === Globals.apiTypes.http && !stage) { + private static _getStage (stage: string, apiType: string, basePath: string) { + if (apiType === Globals.apiTypes.http && (basePath === Globals.defaultBasePath || !stage)) { return Globals.defaultStage; } return stage || Globals.getBaseStage(); diff --git a/test/integration-tests/basic/deploy-idempotent2/serverless.yml b/test/integration-tests/basic/deploy-idempotent2/serverless.yml index df6f94db..25b002b1 100644 --- a/test/integration-tests/basic/deploy-idempotent2/serverless.yml +++ b/test/integration-tests/basic/deploy-idempotent2/serverless.yml @@ -25,7 +25,6 @@ custom: domainName: ${env:PLUGIN_IDENTIFIER}-deploy-idempotent2-${env:RANDOM_STRING}.${env:TEST_DOMAIN} allowPathMatching: true - rest: - autoDomain: true basePath: "path2" domainName: ${env:PLUGIN_IDENTIFIER}-deploy-idempotent2-${env:RANDOM_STRING}.${env:TEST_DOMAIN} allowPathMatching: true diff --git a/test/integration-tests/debug/debug.test.ts b/test/integration-tests/debug/debug.test.ts deleted file mode 100644 index 0c5d128a..00000000 --- a/test/integration-tests/debug/debug.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import "mocha"; -import utilities = require("../test-utilities"); -import { TEMP_DIR } from "../base"; - -const CONFIGS_FOLDER = "debug"; -const TIMEOUT_MINUTES = 15 * 60 * 1000; // 15 minutes in milliseconds - -describe("Integration Tests", function () { - this.timeout(TIMEOUT_MINUTES); - - it("Creates pr-example", async () => { - const testName = "pr-example"; - const configFolder = `${CONFIGS_FOLDER}/${testName}`; - - try { - await utilities.createTempDir(TEMP_DIR, configFolder); - await utilities.slsCreateDomain(TEMP_DIR, true); - await utilities.slsDeploy(TEMP_DIR, true); - } finally { - await utilities.destroyResources(testName); - } - }); -}); diff --git a/test/integration-tests/debug/pr-example/handler.js b/test/integration-tests/debug/pr-example/handler.js deleted file mode 100644 index 1729e0e1..00000000 --- a/test/integration-tests/debug/pr-example/handler.js +++ /dev/null @@ -1,13 +0,0 @@ -"use strict"; - -module.exports.helloWorld = (event, _context, callback) => { - const response = { - statusCode: 200, - body: JSON.stringify({ - message: "Go Serverless! Your function executed successfully!", - input: event, - }), - }; - - callback(null, response); -}; diff --git a/test/integration-tests/debug/pr-example/serverless.yml b/test/integration-tests/debug/pr-example/serverless.yml deleted file mode 100644 index d16aa98e..00000000 --- a/test/integration-tests/debug/pr-example/serverless.yml +++ /dev/null @@ -1,35 +0,0 @@ -service: ${env:PLUGIN_IDENTIFIER}-pr-example -provider: - name: aws - iam: - role: arn:aws:iam::${aws:accountId}:role/sls_domain_manager_lambda - runtime: nodejs16.x - region: us-west-2 - stage: test -functions: - helloWorld: - handler: handler.helloWorld - events: - - http: - path: /hello-world - method: get - -plugins: - - serverless-domain-manager - -custom: - customDomains: - - rest: - autoDomain: true - basePath: "path1" - domainName: ${env:PLUGIN_IDENTIFIER}-rest-debug.${env:TEST_DOMAIN} - allowPathMatching: true - - rest: - autoDomain: true - basePath: "path2" - domainName: ${env:PLUGIN_IDENTIFIER}-rest-debug.${env:TEST_DOMAIN} - allowPathMatching: true - -package: - patterns: - - '!node_modules/**' diff --git a/test/integration-tests/deploy/http-api/serverless.yml b/test/integration-tests/deploy/http-api/serverless.yml index 22123808..69755091 100644 --- a/test/integration-tests/deploy/http-api/serverless.yml +++ b/test/integration-tests/deploy/http-api/serverless.yml @@ -20,6 +20,8 @@ custom: domainName: ${env:PLUGIN_IDENTIFIER}-http-api-${env:RANDOM_STRING}.${env:TEST_DOMAIN} endpointType: REGIONAL apiType: http + # the stage should be set to $default for the base path (none) + stage: dummy_stage package: patterns: diff --git a/test/integration-tests/test-utilities.ts b/test/integration-tests/test-utilities.ts index 2da6889b..4219b75f 100644 --- a/test/integration-tests/test-utilities.ts +++ b/test/integration-tests/test-utilities.ts @@ -3,6 +3,8 @@ import shell = require("shelljs"); import { TEMP_DIR } from "./base"; +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + /** * Executes given shell command. * @param cmd shell command to execute @@ -52,7 +54,9 @@ function slsCreateDomain (tempDir, debug: boolean = false) { * @param debug - enable loging * @returns {Promise} */ -function slsDeploy (tempDir, debug: boolean = false) { +async function slsDeploy (tempDir, debug: boolean = false) { + // sleep to avoid `to many requests` error as we run a lot of tests one after another + await sleep(5000); return exec(`cd ${tempDir} && npx serverless deploy` + (debug ? " --verbose" : "")); }