From d4d8333daae1f42590a1001bf0dee50f2d503b8d Mon Sep 17 00:00:00 2001 From: sromic Date: Fri, 11 Aug 2023 10:06:19 +0200 Subject: [PATCH 1/7] CHANGE - fixed not getting all pagginated results from AWS clients calls --- package-lock.json | 4 +- src/aws/acm-wrapper.ts | 132 ++++++----- src/aws/api-gateway-v1-wrapper.ts | 288 ++++++++++++----------- src/aws/api-gateway-v2-wrapper.ts | 361 +++++++++++++++-------------- src/aws/cloud-formation-wrapper.ts | 328 +++++++++++++------------- src/aws/route53-wrapper.ts | 262 +++++++++++---------- src/utils.ts | 66 ++++-- test/unit-tests/utils.test.ts | 241 +++++++++++++++++++ 8 files changed, 998 insertions(+), 684 deletions(-) create mode 100644 test/unit-tests/utils.test.ts diff --git a/package-lock.json b/package-lock.json index 7678a7bb..0debd573 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "serverless-domain-manager", - "version": "7.1.0", + "version": "7.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "serverless-domain-manager", - "version": "7.1.0", + "version": "7.1.1", "license": "MIT", "dependencies": { "@aws-sdk/client-acm": "^3.370.0", diff --git a/src/aws/acm-wrapper.ts b/src/aws/acm-wrapper.ts index 7af6fdbb..6f1fd06a 100644 --- a/src/aws/acm-wrapper.ts +++ b/src/aws/acm-wrapper.ts @@ -1,82 +1,90 @@ import { - ACMClient, - ListCertificatesCommand, - ListCertificatesCommandOutput, + ACMClient, + CertificateSummary, + ListCertificatesCommand, + ListCertificatesCommandInput, + ListCertificatesCommandOutput } from "@aws-sdk/client-acm"; import Globals from "../globals"; import DomainConfig = require("../models/domain-config"); +import { getAWSPagedResults } from "../utils"; const certStatuses = ["PENDING_VALIDATION", "ISSUED", "INACTIVE"]; 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() - }); - } + 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() + }); + } - 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 response: ListCertificatesCommandOutput = await this.acm.send( - 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(response.CertificateSummaryList, certificateName); - } else { - certificateName = domain.givenDomainName; - certificateArn = this.getCertArnByDomainName(response.CertificateSummaryList, certificateName); - } - } 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; + 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 = this.getCertArnByDomainName(certificates, certificateName); + } + } 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 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 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 59f62809..70613c89 100644 --- a/src/aws/api-gateway-v1-wrapper.ts +++ b/src/aws/api-gateway-v1-wrapper.ts @@ -5,179 +5,185 @@ import DomainConfig = require("../models/domain-config"); import DomainInfo = require("../models/domain-info"); import Globals from "../globals"; import { - APIGatewayClient, - CreateBasePathMappingCommand, - CreateDomainNameCommand, - CreateDomainNameCommandOutput, - DeleteBasePathMappingCommand, - DeleteDomainNameCommand, - GetBasePathMappingsCommand, - GetBasePathMappingsCommandOutput, - GetDomainNameCommand, - GetDomainNameCommandOutput, - UpdateBasePathMappingCommand, + APIGatewayClient, + BasePathMapping, + CreateBasePathMappingCommand, + CreateDomainNameCommand, + CreateDomainNameCommandOutput, + DeleteBasePathMappingCommand, + DeleteDomainNameCommand, + GetBasePathMappingsCommand, + GetBasePathMappingsCommandInput, + GetBasePathMappingsCommandOutput, + GetDomainNameCommand, + GetDomainNameCommandOutput, + UpdateBasePathMappingCommand } from "@aws-sdk/client-api-gateway"; import ApiGatewayMap = require("../models/api-gateway-map"); import APIGatewayBase = require("../models/apigateway-base"); import Logging from "../logging"; +import { getAWSPagedResults } from "../utils"; class APIGatewayV1Wrapper extends APIGatewayBase { - constructor(credentials?: any,) { - super(); - this.apiGateway = new APIGatewayClient({ - credentials, - region: Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy() - }); - } + constructor(credentials?: any) { + super(); + this.apiGateway = new APIGatewayClient({ + credentials, + region: Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() + }); + } - 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, - }) - ); - 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.`); - } + 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}` + ); + } + 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 response: GetBasePathMappingsCommandOutput = await this.apiGateway.send( - new GetBasePathMappingsCommand({ - domainName: domain.givenDomainName - }) - ); - const items = response.items || []; - 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 { - try { - await this.apiGateway.send(new UpdateBasePathMappingCommand({ - basePath: domain.apiMapping.basePath, - domainName: domain.givenDomainName, + public async updateBasePathMapping(domain: DomainConfig): Promise { + try { + await this.apiGateway.send(new UpdateBasePathMappingCommand({ + basePath: domain.apiMapping.basePath, + domainName: domain.givenDomainName, patchOperations: [{ - op: "replace", - path: "/basePath", - value: domain.basePath, + op: "replace", + path: "/basePath", + value: domain.basePath, }] } - )); - Logging.logInfo(`V1 - Updated API mapping from '${domain.apiMapping.basePath}' + )); + Logging.logInfo(`V1 - Updated API mapping from '${domain.apiMapping.basePath}' to '${domain.basePath}' for '${domain.givenDomainName}'`); - } catch (err) { - throw new Error( - `V1 - Unable to update base path mapping for '${domain.givenDomainName}':\n${err.message}` - ); - } + } 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 18ea71d3..4cd3f904 100644 --- a/src/aws/api-gateway-v2-wrapper.ts +++ b/src/aws/api-gateway-v2-wrapper.ts @@ -7,207 +7,212 @@ import Globals from "../globals"; import ApiGatewayMap = require("../models/api-gateway-map"); import APIGatewayBase = require("../models/apigateway-base"); import { - ApiGatewayV2Client, - CreateApiMappingCommand, - CreateDomainNameCommand, - CreateDomainNameCommandOutput, - DeleteApiMappingCommand, - DeleteDomainNameCommand, - GetApiMappingsCommand, - GetApiMappingsCommandOutput, - GetDomainNameCommand, - GetDomainNameCommandOutput, - UpdateApiMappingCommand, + ApiGatewayV2Client, + ApiMapping, + CreateApiMappingCommand, + CreateDomainNameCommand, + CreateDomainNameCommandOutput, + DeleteApiMappingCommand, + DeleteDomainNameCommand, + GetApiMappingsCommand, + GetApiMappingsCommandInput, + GetApiMappingsCommandOutput, + GetDomainNameCommand, + GetDomainNameCommandOutput, + UpdateApiMappingCommand } from "@aws-sdk/client-apigatewayv2"; import Logging from "../logging"; +import { getAWSPagedResults } from "../utils"; class APIGatewayV2Wrapper extends APIGatewayBase { + constructor(credentials?: any) { + super(); + this.apiGateway = new ApiGatewayV2Client({ + credentials, + region: Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() + }); + } - constructor(credentials?: any) { - super(); - this.apiGateway = new ApiGatewayV2Client({ - credentials, - region: Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy() - }); - } - - /** - * 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 - }; + const isEdgeType = domain.endpointType === Globals.endpointTypes.edge; + if (!isEdgeType && 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( - `V2 - 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( + `V2 - Failed to create custom domain '${domain.givenDomainName}':\n${err.message}` + ); } + } - /** - * 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.`); - } + /** + * 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.`); } + } - /** - * 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}` - ); - } + /** + * 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}` + ); } + } - /** - * 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}` - ); - } + /** + * 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}` + ); } + } - /** - * Get APi Mapping - * @param domain: DomainConfig - */ - public async getBasePathMappings(domain: DomainConfig): Promise { - try { - const response: GetApiMappingsCommandOutput = await this.apiGateway.send( - new GetApiMappingsCommand({ - DomainName: domain.givenDomainName - }) - ); - const items = response.Items || []; - 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}` - ); - } + /** + * 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}` + ); } + } - /** - * 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}` - ); - } + /** + * 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}` + ); } + } - /** - * Delete Api Mapping - */ - public async deleteBasePathMapping(domain: DomainConfig): Promise { - try { - await this.apiGateway.send(new DeleteApiMappingCommand({ - ApiMappingId: domain.apiMapping.apiMappingId, - DomainName: domain.givenDomainName, + /** + * 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}` - ); - } + 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 4f298a12..57220569 100644 --- a/src/aws/cloud-formation-wrapper.ts +++ b/src/aws/cloud-formation-wrapper.ts @@ -2,186 +2,202 @@ * Wrapper class for AWS CloudFormation provider */ -import Globals from "../globals"; -import Logging from "../logging"; import { - CloudFormationClient, - DescribeStackResourceCommand, - DescribeStackResourceCommandOutput, - DescribeStacksCommand, - DescribeStacksCommandOutput, - ListExportsCommand, - ListExportsCommandOutput + CloudFormationClient, + DescribeStackResourceCommand, + DescribeStackResourceCommandOutput, + DescribeStacksCommand, + DescribeStacksCommandInput, + DescribeStacksCommandOutput, + Export, + ListExportsCommand, + ListExportsCommandInput, + ListExportsCommandOutput, + Stack } from "@aws-sdk/client-cloudformation"; +import Globals from "../globals"; +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() - }); + 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() + }); + } + + /** + * 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]; - /** - * 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; - } + if (apiGatewayValue) { + if (typeof apiGatewayValue === "string") { + return apiGatewayValue; + } - return await this.getCloudformationId(apiGatewayValue, apiType); - } + return await this.getCloudformationId(apiGatewayValue, apiType); + } - 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`); + } + return nameValue; } - 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; - } - } + 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; + } + } - // log warning not supported restApiId - Logging.logWarning(`Unsupported apiGateway.${apiType} object`); + // log warning not supported restApiId + Logging.logWarning(`Unsupported apiGateway.${apiType} object`); - return null; + return null; + } + + /** + * 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 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; - 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); - } - - if (!response) { - throw new Error(`Failed to find a stack ${this.stackName}\n`); - } - - const apiId = response.StackResourceDetail.PhysicalResourceId; - if (!apiId) { - throw new Error(`No ApiId associated with CloudFormation stack ${this.stackName}`); - } - - return apiId; + 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); } - /** - * Gets values by names from cloudformation exports - */ - public async getImportValues(names: string[]): Promise { - const response: ListExportsCommandOutput = await this.cloudFormation.send( - new ListExportsCommand({}) - ); - const exports = response.Exports || []; - // 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}), {}); + if (!response) { + throw new Error(`Failed to find a stack ${this.stackName}\n`); } - /** - * 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`); - } + const apiId = response.StackResourceDetail.PhysicalResourceId; + if (!apiId) { + throw new Error(`No ApiId associated with CloudFormation stack ${this.stackName}`); } - /** - * 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 response: DescribeStacksCommandOutput = await this.cloudFormation.send( - new DescribeStacksCommand({}) - ); - const stacks = response.Stacks || []; - - // 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 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 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; + } + const match = stack.RootId.match(regex); + if (match) { + acc.push(stack.StackName); } - } - return null; + 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; + } } export = CloudFormationWrapper; diff --git a/src/aws/route53-wrapper.ts b/src/aws/route53-wrapper.ts index 37b67d4d..c5fddced 100644 --- a/src/aws/route53-wrapper.ts +++ b/src/aws/route53-wrapper.ts @@ -2,150 +2,160 @@ import Globals from "../globals"; import DomainConfig = require("../models/domain-config"); import Logging from "../logging"; import { - ChangeResourceRecordSetsCommand, - ListHostedZonesCommand, - ListHostedZonesCommandOutput, - Route53Client + ChangeResourceRecordSetsCommand, + HostedZone, + ListHostedZonesCommand, + ListHostedZonesCommandInput, + ListHostedZonesCommandOutput, + Route53Client } from "@aws-sdk/client-route-53"; +import { getAWSPagedResults } from "../utils"; class Route53Wrapper { - public route53: Route53Client; + public route53: Route53Client; - constructor(credentials?: any, region?: string) { - // not null and not undefined - if (credentials) { - this.route53 = new Route53Client({ - credentials, - region: region || Globals.getRegion() - }); - } else { - this.route53 = new Route53Client({region: Globals.getRegion()}); - } + constructor(credentials?: any, region?: string) { + // not null and not undefined + if (credentials) { + this.route53 = new Route53Client({ + credentials, + region: region || Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() + }); + } else { + this.route53 = new Route53Client({ + region: Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() + }); } + } - /** - * 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 { - const response: ListHostedZonesCommandOutput = await this.route53.send( - new ListHostedZonesCommand({}) - ); - hostedZones = response.HostedZones || hostedZones; - } 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({}) + ); + } catch (err) { + throw new Error(`Unable to list hosted zones in Route53.\n${err.message}`); + } - const targetHostedZone = hostedZones - .filter((hostedZone) => { - return !isPrivateDefined || isHostedZonePrivate === hostedZone.Config.PrivateZone; - }) - .filter((hostedZone) => { - const hostedZoneName = hostedZone.Name.replace(/\.$/, ""); - return domain.givenDomainName.endsWith(hostedZoneName); - }) - .sort((zone1, zone2) => zone2.Name.length - zone1.Name.length) - .shift(); + const targetHostedZone = hostedZones + .filter((hostedZone) => { + return !isPrivateDefined || isHostedZonePrivate === hostedZone.Config.PrivateZone; + }) + .filter((hostedZone) => { + const hostedZoneName = hostedZone.Name.replace(/\.$/, ""); + return domain.givenDomainName.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: string, domain: DomainConfig): Promise { - if (domain.createRoute53Record === false) { - Logging.logInfo(`Skipping ${action === "DELETE" ? "removal" : "creation"} of Route53 record.`); - return; - } - // 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, - }; + /** + * 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: string, domain: DomainConfig): Promise { + if (domain.createRoute53Record === false) { + Logging.logInfo(`Skipping ${action === "DELETE" ? "removal" : "creation"} of Route53 record.`); + return; + } + // 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: await this.route53.config.region(), - SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, - ...route53healthCheck, - }; - } + let routingOptions = {}; + if (route53Params.routingPolicy === Globals.routingPolicies.latency) { + routingOptions = { + Region: await this.route53.config.region(), + SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, + ...route53healthCheck, + }; + } - if (route53Params.routingPolicy === Globals.routingPolicies.weighted) { - routingOptions = { - Weight: route53Params.weight, - SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, - ...route53healthCheck, - }; - } + if (route53Params.routingPolicy === Globals.routingPolicies.weighted) { + routingOptions = { + Weight: route53Params.weight, + 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]; - } + let hostedZoneIds: string[]; + if (domain.splitHorizonDns) { + hostedZoneIds = await Promise.all([ + this.getRoute53HostedZoneId(domain, false), + this.getRoute53HostedZoneId(domain, true), + ]); + } else { + hostedZoneIds = [route53HostedZoneId]; + } - const recordsToCreate = domain.createRoute53IPv6Record ? ["A", "AAAA"] : ["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, - }, - })); + const recordsToCreate = domain.createRoute53IPv6Record ? ["A", "AAAA"] : ["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, + }, + })); - 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}` - ); - } - } + 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/utils.ts b/src/utils.ts index f1c762e7..0e4af195 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,4 @@ +import { Client, Command } from "@smithy/smithy-client"; import Globals from "./globals"; /** @@ -5,8 +6,8 @@ import Globals from "./globals"; * @param seconds * @returns {Promise} Resolves after given number of seconds. */ -async function sleep(seconds) { - return new Promise((resolve) => setTimeout(resolve, 1000 * seconds)); +async function sleep(seconds: number) { + return new Promise((resolve) => setTimeout(resolve, 1000 * seconds)); } /** @@ -21,23 +22,50 @@ async function sleep(seconds) { * @returns {boolean} the parsed boolean from the config value, or the default value */ function evaluateBoolean(value: any, defaultValue: boolean): boolean { - if (value === undefined) { - return defaultValue; - } + if (value === undefined) { + return defaultValue; + } - const s = value.toString().toLowerCase().trim(); - const trueValues = ["true", "1"]; - const falseValues = ["false", "0"]; - if (trueValues.indexOf(s) >= 0) { - return true; - } - if (falseValues.indexOf(s) >= 0) { - return false; - } - throw new Error(`${Globals.pluginName}: Ambiguous boolean config: "${value}"`); + const s = value.toString().toLowerCase().trim(); + const trueValues = ["true", "1"]; + const falseValues = ["false", "0"]; + if (trueValues.indexOf(s) >= 0) { + return true; + } + if (falseValues.indexOf(s) >= 0) { + return false; + } + throw new Error(`${Globals.pluginName}: Ambiguous boolean config: "${value}"`); } -export { - evaluateBoolean, - sleep, -}; +/** + * Iterate through the pages of a AWS SDK response and collect them into a single array + * + * @param client - The AWS service instance to use to make the calls + * @param resultsKey - The key name in the response that contains the items to return + * @param nextTokenKey - The request key name to append to the request that has the paging token value + * @param nextRequestTokenKey - The response key name that has the next paging token value + * @param params - Parameters to send in the request + */ +async function getAWSPagedResults( + client: Client, + resultsKey: keyof ClientOutputCommand, + nextTokenKey: keyof ClientInputCommand, + nextRequestTokenKey: keyof ClientOutputCommand, + params: Command +): Promise { + let results = []; + let response = await client.send(params); + results = results.concat(response[resultsKey] || results); + while ( + response.hasOwnProperty(nextRequestTokenKey) && + response[nextRequestTokenKey] + ) { + params.input[nextTokenKey] = response[nextRequestTokenKey]; + response = await client.send(params); + results = results.concat(response[resultsKey]); + } + return results; +} + +export { evaluateBoolean, sleep, getAWSPagedResults }; diff --git a/test/unit-tests/utils.test.ts b/test/unit-tests/utils.test.ts new file mode 100644 index 00000000..ee4260e7 --- /dev/null +++ b/test/unit-tests/utils.test.ts @@ -0,0 +1,241 @@ +import { expect } from "./base"; +import { getAWSPagedResults } from "../../src/utils"; +import { mockClient } from "aws-sdk-client-mock"; +import { ACMClient, ListCertificatesCommand } from "@aws-sdk/client-acm"; +import { Client } from "@aws-sdk/smithy-client"; +import { + APIGatewayClient, + GetBasePathMappingsCommand, +} from "@aws-sdk/client-api-gateway"; +import { + ApiGatewayV2Client, + GetApiMappingsCommand, +} from "@aws-sdk/client-apigatewayv2"; +import { + CloudFormationClient, + ListExportsCommand, +} from "@aws-sdk/client-cloudformation"; +import { + ListHostedZonesCommand, + Route53Client, +} from "@aws-sdk/client-route-53"; + +describe("Utils checks", () => { + describe("acm-wrapper", () => { + it("get all certificates", async () => { + const ACMCMock = mockClient(ACMClient); + ACMCMock.on(ListCertificatesCommand) + .resolvesOnce({ + CertificateSummaryList: [ + { + CertificateArn: "test_certificate_arn", + DomainName: "test_domain", + Status: "ISSUED", + }, + ], + NextToken: + '{"CertificateArn": "test_certificate_arn2","DomainName": "test_domain2","Status": "ISSUED"}', + }) + .resolves({ + CertificateSummaryList: [ + { + CertificateArn: "test_certificate_arn2", + DomainName: "test_domain2", + Status: "ISSUED", + }, + ], + }); + + const certStatuses = ["PENDING_VALIDATION", "ISSUED", "INACTIVE"]; + + const certs = await getAWSPagedResults( + ACMCMock as unknown as Client, + "CertificateSummaryList", + "NextToken", + "NextToken", + new ListCertificatesCommand({ CertificateStatuses: certStatuses }) + ); + expect(certs.length).to.equal(2); + expect(ACMCMock.calls().length).to.equal(2); + }); + }); + + describe("api-gateway-v1-wrapper", () => { + it("get all base path mappings", async () => { + const APIGatewayCMock = mockClient(APIGatewayClient); + APIGatewayCMock.on(GetBasePathMappingsCommand) + .resolvesOnce({ + items: [ + { + restApiId: "1", + basePath: "test_domain", + stage: "mock", + }, + ], + position: "position", + }) + .resolves({ + items: [ + { + restApiId: "2", + basePath: "test_domain2", + stage: "mock", + }, + ], + }); + + const items = await getAWSPagedResults( + APIGatewayCMock as unknown as Client, + "items", + "position", + "position", + new GetBasePathMappingsCommand({ + domainName: "domain", + }) + ); + expect(items.length).to.equal(2); + expect(APIGatewayCMock.calls().length).to.equal(2); + }); + }); + + describe("api-gateway-v2-wrapper", () => { + it("get all api mappings", async () => { + const APIGatewayV2CMock = mockClient(ApiGatewayV2Client); + APIGatewayV2CMock.on(GetApiMappingsCommand) + .resolvesOnce({ + Items: [ + { + ApiId: "ApiId", + ApiMappingKey: "ApiMappingKey", + Stage: "mock", + ApiMappingId: "ApiMappingId", + }, + ], + NextToken: 'NextToken', + }) + .resolvesOnce({ + Items: [ + { + ApiId: "ApiId4", + ApiMappingKey: "ApiMappingKey4", + Stage: "mock", + ApiMappingId: "ApiMappingId4", + }, + ], + NextToken: 'NextToken', + }) + .resolves({ + Items: [ + { + ApiId: "ApiId2", + ApiMappingKey: "ApiMappingKey2", + Stage: "mock", + ApiMappingId: "ApiMappingId2", + }, + { + ApiId: "ApiId3", + ApiMappingKey: "ApiMappingKey3", + Stage: "mock", + ApiMappingId: "ApiMappingId3", + }, + ], + }); + + const items = await getAWSPagedResults( + APIGatewayV2CMock as unknown as Client, + "Items", + "NextToken", + "NextToken", + new GetApiMappingsCommand({ + DomainName: "domain", + }) + ); + expect(items.length).to.equal(4); + expect(APIGatewayV2CMock.calls().length).to.equal(3); + }); + }); + + describe("cloud-formation-wrapper", () => { + it("get all exports", async () => { + const CloudFormationCMock = mockClient(CloudFormationClient); + CloudFormationCMock.on(ListExportsCommand) + .resolvesOnce({ + Exports: [ + { + Name: "Name1", + }, + { + Name: "Name4", + }, + ], + NextToken: 'NextToken', + }) + .resolves({ + Exports: [ + { + Name: "Name2", + }, + { + Name: "Name3", + }, + ], + }); + + const items = await getAWSPagedResults( + CloudFormationCMock as unknown as Client, + "Exports", + "NextToken", + "NextToken", + new ListExportsCommand({}) + ); + expect(items.length).to.equal(4); + expect(CloudFormationCMock.calls().length).to.equal(2); + }); + }); + + describe("route53-wrapper", () => { + it("get all hosted zones", async () => { + const Route53CMock = mockClient(Route53Client); + Route53CMock.on(ListHostedZonesCommand) + .resolvesOnce({ + HostedZones: [ + { + Id: "Id1", + Name: "Name1", + CallerReference: "CallerReference1", + }, + { + Id: "Id2", + Name: "Name2", + CallerReference: "CallerReference2", + }, + ], + NextMarker: 'NextMarker', + }) + .resolves({ + HostedZones: [ + { + Id: "Id3", + Name: "Name3", + CallerReference: "CallerReference3", + }, + { + Id: "Id4", + Name: "Name4", + CallerReference: "CallerReference4", + }, + ], + }); + + const items = await getAWSPagedResults( + Route53CMock as unknown as Client, + "HostedZones", + "Marker", + "NextMarker", + new ListHostedZonesCommand({}) + ); + expect(items.length).to.equal(4); + expect(Route53CMock.calls().length).to.equal(2); + }); + }); +}); From ab87c7086b831f43ec3568c5d1c3584ad7cba63c Mon Sep 17 00:00:00 2001 From: sromic Date: Fri, 11 Aug 2023 10:36:21 +0200 Subject: [PATCH 2/7] CHANGE - reverted style formatting --- src/aws/acm-wrapper.ts | 126 +++++------ src/aws/api-gateway-v1-wrapper.ts | 278 +++++++++++------------ src/aws/api-gateway-v2-wrapper.ts | 342 ++++++++++++++--------------- src/aws/cloud-formation-wrapper.ts | 320 +++++++++++++-------------- src/aws/route53-wrapper.ts | 258 +++++++++++----------- src/utils.ts | 28 +-- 6 files changed, 676 insertions(+), 676 deletions(-) diff --git a/src/aws/acm-wrapper.ts b/src/aws/acm-wrapper.ts index 6f1fd06a..f5ba0023 100644 --- a/src/aws/acm-wrapper.ts +++ b/src/aws/acm-wrapper.ts @@ -12,79 +12,79 @@ import { getAWSPagedResults } from "../utils"; const certStatuses = ["PENDING_VALIDATION", "ISSUED", "INACTIVE"]; 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() - }); - } + 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() + }); + } - 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 = this.getCertArnByDomainName(certificates, certificateName); - } - } 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 = this.getCertArnByDomainName(certificates, certificateName); + } + } 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; } - return certificateArn; - } - private getCertArnByCertName(certificates, certName): string { - const found = certificates.find((c) => c.DomainName === certName); - if (found) { - return found.CertificateArn; + private getCertArnByCertName(certificates, certName): string { + const found = certificates.find((c) => c.DomainName === certName); + if (found) { + return found.CertificateArn; + } + return null; } - return null; - } - private 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 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 70613c89..8d85566d 100644 --- a/src/aws/api-gateway-v1-wrapper.ts +++ b/src/aws/api-gateway-v1-wrapper.ts @@ -25,165 +25,165 @@ import Logging from "../logging"; import { getAWSPagedResults } from "../utils"; class APIGatewayV1Wrapper extends APIGatewayBase { - constructor(credentials?: any) { - super(); - this.apiGateway = new APIGatewayClient({ - credentials, - region: Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy() - }); - } + constructor(credentials?: any) { + super(); + this.apiGateway = new APIGatewayClient({ + credentials, + region: Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() + }); + } - 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, - }) - ); - 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.`); + 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}` + ); + } + 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. - Unable to create base path mapping for '${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. + 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. - Unable to get Base Path Mappings:\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. + Unable to get Base Path Mappings:\n${err.message}` + ); + } } - } - public async updateBasePathMapping(domain: DomainConfig): Promise { - try { - await this.apiGateway.send(new UpdateBasePathMappingCommand({ - basePath: domain.apiMapping.basePath, - domainName: domain.givenDomainName, - patchOperations: [{ - op: "replace", - path: "/basePath", - value: domain.basePath, - }] - } - )); - Logging.logInfo(`V1 - Updated API mapping from '${domain.apiMapping.basePath}' - to '${domain.basePath}' for '${domain.givenDomainName}'`); - } catch (err) { - throw new Error( - `V1 - Unable to update base path mapping for '${domain.givenDomainName}':\n${err.message}` - ); + public async updateBasePathMapping(domain: DomainConfig): Promise { + try { + await this.apiGateway.send(new UpdateBasePathMappingCommand({ + basePath: domain.apiMapping.basePath, + domainName: domain.givenDomainName, + patchOperations: [{ + op: "replace", + path: "/basePath", + value: domain.basePath, + }] + } + )); + Logging.logInfo(`V1 - Updated API mapping from '${domain.apiMapping.basePath}' + to '${domain.basePath}' for '${domain.givenDomainName}'`); + } 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 4cd3f904..1d1c3065 100644 --- a/src/aws/api-gateway-v2-wrapper.ts +++ b/src/aws/api-gateway-v2-wrapper.ts @@ -25,194 +25,194 @@ import Logging from "../logging"; import { getAWSPagedResults } from "../utils"; class APIGatewayV2Wrapper extends APIGatewayBase { - constructor(credentials?: any) { - super(); - this.apiGateway = new ApiGatewayV2Client({ - credentials, - region: Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy() - }); - } + constructor(credentials?: any) { + super(); + this.apiGateway = new ApiGatewayV2Client({ + credentials, + region: Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() + }); + } - /** - * 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 - }; + const isEdgeType = domain.endpointType === Globals.endpointTypes.edge; + if (!isEdgeType && 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( + `V2 - 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( - `V2 - Failed to create custom domain '${domain.givenDomainName}':\n${err.message}` - ); + /** + * 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.`); + } } - } - /** - * 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.`); + /** + * 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}` + ); + } } - } - /** - * 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}` - ); + /** + * 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}` + ); + } } - } - /** - * 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}` - ); + /** + * 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}` + ); + } } - } - /** - * 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}` - ); + /** + * 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}` + ); + } } - } - /** - * 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}` - ); - } - } - - /** - * 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}` - ); + /** + * 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 57220569..8593222b 100644 --- a/src/aws/cloud-formation-wrapper.ts +++ b/src/aws/cloud-formation-wrapper.ts @@ -20,184 +20,184 @@ 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() - }); - } - - /** - * 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; + 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() + }); } - return await this.getStackApiId(apiType); - } + /** + * 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 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; - } - - return await this.getCloudformationId(apiGatewayValue, apiType); - } - - 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`); - } - return nameValue; + return await this.getStackApiId(apiType); } - 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; - } - } + /** + * 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]; - // log warning not supported restApiId - Logging.logWarning(`Unsupported apiGateway.${apiType} object`); + if (apiGatewayValue) { + if (typeof apiGatewayValue === "string") { + return apiGatewayValue; + } - return null; - } + return await this.getCloudformationId(apiGatewayValue, apiType); + } - /** - * 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]; + return null; } - 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); + 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; + } + } + + // log warning not supported restApiId + Logging.logWarning(`Unsupported apiGateway.${apiType} object`); + + return null; } - if (!response) { - throw new Error(`Failed to find a stack ${this.stackName}\n`); + /** + * 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; + 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); + } + + if (!response) { + throw new Error(`Failed to find a stack ${this.stackName}\n`); + } + + const apiId = response.StackResourceDetail.PhysicalResourceId; + if (!apiId) { + throw new Error(`No ApiId associated with CloudFormation stack ${this.stackName}`); + } + + return apiId; } - const apiId = response.StackResourceDetail.PhysicalResourceId; - if (!apiId) { - throw new Error(`No ApiId associated with CloudFormation stack ${this.stackName}`); + /** + * 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}), {}); } - 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 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; - } - 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); - } + + /** + * 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; + } + 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 c5fddced..f3d9526e 100644 --- a/src/aws/route53-wrapper.ts +++ b/src/aws/route53-wrapper.ts @@ -12,150 +12,150 @@ import { import { getAWSPagedResults } from "../utils"; class Route53Wrapper { - public route53: Route53Client; + public route53: Route53Client; - constructor(credentials?: any, region?: string) { - // not null and not undefined - if (credentials) { - this.route53 = new Route53Client({ - credentials, - region: region || Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy() - }); - } else { - this.route53 = new Route53Client({ - region: Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy() - }); + constructor(credentials?: any, region?: string) { + // not null and not undefined + if (credentials) { + this.route53 = new Route53Client({ + credentials, + region: region || Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() + }); + } else { + this.route53 = new Route53Client({ + region: Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() + }); + } } - } - /** - * 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({}) - ); - } 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({}) + ); + } catch (err) { + throw new Error(`Unable to list hosted zones in Route53.\n${err.message}`); + } - const targetHostedZone = hostedZones - .filter((hostedZone) => { - return !isPrivateDefined || isHostedZonePrivate === hostedZone.Config.PrivateZone; - }) - .filter((hostedZone) => { - const hostedZoneName = hostedZone.Name.replace(/\.$/, ""); - return domain.givenDomainName.endsWith(hostedZoneName); - }) - .sort((zone1, zone2) => zone2.Name.length - zone1.Name.length) - .shift(); + const targetHostedZone = hostedZones + .filter((hostedZone) => { + return !isPrivateDefined || isHostedZonePrivate === hostedZone.Config.PrivateZone; + }) + .filter((hostedZone) => { + const hostedZoneName = hostedZone.Name.replace(/\.$/, ""); + return domain.givenDomainName.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: string, domain: DomainConfig): Promise { - if (domain.createRoute53Record === false) { - Logging.logInfo(`Skipping ${action === "DELETE" ? "removal" : "creation"} of Route53 record.`); - return; - } - // 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, - }; + /** + * 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: string, domain: DomainConfig): Promise { + if (domain.createRoute53Record === false) { + Logging.logInfo(`Skipping ${action === "DELETE" ? "removal" : "creation"} of Route53 record.`); + return; + } + // 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: await this.route53.config.region(), - SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, - ...route53healthCheck, - }; - } + let routingOptions = {}; + if (route53Params.routingPolicy === Globals.routingPolicies.latency) { + routingOptions = { + Region: await this.route53.config.region(), + SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, + ...route53healthCheck, + }; + } - if (route53Params.routingPolicy === Globals.routingPolicies.weighted) { - routingOptions = { - Weight: route53Params.weight, - SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, - ...route53healthCheck, - }; - } + if (route53Params.routingPolicy === Globals.routingPolicies.weighted) { + routingOptions = { + Weight: route53Params.weight, + 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]; - } + let hostedZoneIds: string[]; + if (domain.splitHorizonDns) { + hostedZoneIds = await Promise.all([ + this.getRoute53HostedZoneId(domain, false), + this.getRoute53HostedZoneId(domain, true), + ]); + } else { + hostedZoneIds = [route53HostedZoneId]; + } - const recordsToCreate = domain.createRoute53IPv6Record ? ["A", "AAAA"] : ["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, - }, - })); + const recordsToCreate = domain.createRoute53IPv6Record ? ["A", "AAAA"] : ["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, + }, + })); - 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}` - ); - } + 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/utils.ts b/src/utils.ts index 0e4af195..3cfbc7a1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,7 +7,7 @@ import Globals from "./globals"; * @returns {Promise} Resolves after given number of seconds. */ async function sleep(seconds: number) { - return new Promise((resolve) => setTimeout(resolve, 1000 * seconds)); + return new Promise((resolve) => setTimeout(resolve, 1000 * seconds)); } /** @@ -22,20 +22,20 @@ async function sleep(seconds: number) { * @returns {boolean} the parsed boolean from the config value, or the default value */ function evaluateBoolean(value: any, defaultValue: boolean): boolean { - if (value === undefined) { - return defaultValue; - } + if (value === undefined) { + return defaultValue; + } - const s = value.toString().toLowerCase().trim(); - const trueValues = ["true", "1"]; - const falseValues = ["false", "0"]; - if (trueValues.indexOf(s) >= 0) { - return true; - } - if (falseValues.indexOf(s) >= 0) { - return false; - } - throw new Error(`${Globals.pluginName}: Ambiguous boolean config: "${value}"`); + const s = value.toString().toLowerCase().trim(); + const trueValues = ["true", "1"]; + const falseValues = ["false", "0"]; + if (trueValues.indexOf(s) >= 0) { + return true; + } + if (falseValues.indexOf(s) >= 0) { + return false; + } + throw new Error(`${Globals.pluginName}: Ambiguous boolean config: "${value}"`); } /** From a72afe6cf9548ffa993bd7e5e9ef0d178bc3797b Mon Sep 17 00:00:00 2001 From: sromic Date: Fri, 11 Aug 2023 11:02:47 +0200 Subject: [PATCH 3/7] CHANGE - reverted code style formatting --- src/aws/acm-wrapper.ts | 68 +++++------ src/aws/api-gateway-v1-wrapper.ts | 182 ++++++++++++++-------------- src/aws/api-gateway-v2-wrapper.ts | 186 ++++++++++++++--------------- src/aws/cloud-formation-wrapper.ts | 92 +++++++------- src/aws/route53-wrapper.ts | 154 ++++++++++++------------ src/utils.ts | 6 +- 6 files changed, 342 insertions(+), 346 deletions(-) diff --git a/src/aws/acm-wrapper.ts b/src/aws/acm-wrapper.ts index f5ba0023..5710da43 100644 --- a/src/aws/acm-wrapper.ts +++ b/src/aws/acm-wrapper.ts @@ -17,9 +17,9 @@ class ACMWrapper { 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() + credentials, + region: isEdge ? Globals.defaultRegion : Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() }); } @@ -38,52 +38,52 @@ class ACMWrapper { // enhancement idea: weight the choice of cert so longer expires // and RenewalEligibility = ELIGIBLE is more preferable if (certificateName) { - certificateArn = this.getCertArnByCertName(certificates, certificateName); + certificateArn = this.getCertArnByCertName(certificates, certificateName); } else { - certificateName = domain.givenDomainName; - certificateArn = this.getCertArnByDomainName(certificates, certificateName); + certificateName = domain.givenDomainName; + certificateArn = this.getCertArnByDomainName(certificates, certificateName); } } catch (err) { - throw Error(`Could not search certificates in Certificate Manager.\n${err.message}`); + 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}'.`); + 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; + const found = certificates.find((c) => c.DomainName === certName); + if (found) { + return found.CertificateArn; + } + return null; } private 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; + // 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; } } diff --git a/src/aws/api-gateway-v1-wrapper.ts b/src/aws/api-gateway-v1-wrapper.ts index 8d85566d..d56172fa 100644 --- a/src/aws/api-gateway-v1-wrapper.ts +++ b/src/aws/api-gateway-v1-wrapper.ts @@ -28,160 +28,160 @@ class APIGatewayV1Wrapper extends APIGatewayBase { constructor(credentials?: any) { super(); this.apiGateway = new APIGatewayClient({ - credentials, - region: Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy() + credentials, + region: Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() }); } public async createCustomDomain(domain: DomainConfig): Promise { const providerTags = { - ...Globals.serverless.service.provider.stackTags, - ...Globals.serverless.service.provider.tags + ...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, + 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; + params.certificateArn = domain.certificateArn; } else { - params.regionalCertificateArn = domain.certificateArn; + 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); + return new DomainInfo(domainInfo); } catch (err) { - throw new Error( - `V1 - Failed to create custom domain '${domain.givenDomainName}':\n${err.message}` - ); + 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, - }) - ); - 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}` + const domainInfo: GetDomainNameCommandOutput = await this.apiGateway.send( + new GetDomainNameCommand({ + domainName: domain.givenDomainName, + }) ); - } - Logging.logWarning(`V1 - '${domain.givenDomainName}' does not exist.`); + 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.`); } } public async deleteCustomDomain(domain: DomainConfig): Promise { // Make API call try { - await this.apiGateway.send(new DeleteDomainNameCommand({ - domainName: domain.givenDomainName, - })); + 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}`); + 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}'`); + 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}` - ); + 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); - } - ); + 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}` - ); + 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 { try { await this.apiGateway.send(new UpdateBasePathMappingCommand({ - basePath: domain.apiMapping.basePath, - domainName: domain.givenDomainName, - patchOperations: [{ - op: "replace", - path: "/basePath", - value: domain.basePath, - }] - } - )); - Logging.logInfo(`V1 - Updated API mapping from '${domain.apiMapping.basePath}' - to '${domain.basePath}' for '${domain.givenDomainName}'`); + basePath: domain.apiMapping.basePath, + domainName: domain.givenDomainName, + patchOperations: [{ + op: "replace", + path: "/basePath", + value: domain.basePath, + }] + } + )); + Logging.logInfo(`V1 - Updated API mapping from '${domain.apiMapping.basePath}' + to '${domain.basePath}' for '${domain.givenDomainName}'`); } catch (err) { - throw new Error( - `V1 - Unable to update base path mapping for '${domain.givenDomainName}':\n${err.message}` - ); + 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`); + 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}` - ); + throw new Error( + `V1 - Unable to remove base path mapping for '${domain.givenDomainName}':\n${err.message}` + ); } } } diff --git a/src/aws/api-gateway-v2-wrapper.ts b/src/aws/api-gateway-v2-wrapper.ts index 1d1c3065..310a4c8d 100644 --- a/src/aws/api-gateway-v2-wrapper.ts +++ b/src/aws/api-gateway-v2-wrapper.ts @@ -28,9 +28,9 @@ class APIGatewayV2Wrapper extends APIGatewayBase { constructor(credentials?: any) { super(); this.apiGateway = new ApiGatewayV2Client({ - credentials, - region: Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy() + credentials, + region: Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() }); } @@ -40,29 +40,29 @@ class APIGatewayV2Wrapper extends APIGatewayBase { */ public async createCustomDomain(domain: DomainConfig): Promise { const providerTags = { - ...Globals.serverless.service.provider.stackTags, - ...Globals.serverless.service.provider.tags + ...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 + 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 - }; + params.MutualTlsAuthentication = { + TruststoreUri: domain.tlsTruststoreUri + }; - if (domain.tlsTruststoreVersion) { - params.MutualTlsAuthentication.TruststoreVersion = domain.tlsTruststoreVersion; - } + if (domain.tlsTruststoreVersion) { + params.MutualTlsAuthentication.TruststoreVersion = domain.tlsTruststoreVersion; + } } try { @@ -71,9 +71,9 @@ class APIGatewayV2Wrapper extends APIGatewayBase { ); return new DomainInfo(domainInfo); } catch (err) { - throw new Error( - `V2 - Failed to create custom domain '${domain.givenDomainName}':\n${err.message}` - ); + throw new Error( + `V2 - Failed to create custom domain '${domain.givenDomainName}':\n${err.message}` + ); } } @@ -84,19 +84,19 @@ class APIGatewayV2Wrapper extends APIGatewayBase { 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}` + const domainInfo: GetDomainNameCommandOutput = await this.apiGateway.send( + new GetDomainNameCommand({ + DomainName: domain.givenDomainName + }) ); - } - Logging.logInfo(`V2 - '${domain.givenDomainName}' does not exist.`); + 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.`); } } @@ -107,15 +107,15 @@ class APIGatewayV2Wrapper extends APIGatewayBase { public async deleteCustomDomain(domain: DomainConfig): Promise { // Make API call try { - await this.apiGateway.send( - new DeleteDomainNameCommand({ - DomainName: domain.givenDomainName, - }) - ); + 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}` - ); + throw new Error( + `V2 - Failed to delete custom domain '${domain.givenDomainName}':\n${err.message}` + ); } } @@ -125,27 +125,27 @@ class APIGatewayV2Wrapper extends APIGatewayBase { */ 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' - ) + 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}'`); + 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}` - ); + throw new Error( + `V2 - Unable to create base path mapping for '${domain.givenDomainName}':\n${err.message}` + ); } } @@ -155,22 +155,22 @@ class APIGatewayV2Wrapper extends APIGatewayBase { */ 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) - ); + 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}` - ); + throw new Error( + `V2 - Make sure the '${domain.givenDomainName}' exists. Unable to get API Mappings:\n${err.message}` + ); } } @@ -180,20 +180,20 @@ class APIGatewayV2Wrapper extends APIGatewayBase { */ 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}'`); + 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}` - ); + throw new Error( + `V2 - Unable to update base path mapping for '${domain.givenDomainName}':\n${err.message}` + ); } } @@ -203,14 +203,14 @@ class APIGatewayV2Wrapper extends APIGatewayBase { public async deleteBasePathMapping(domain: DomainConfig): Promise { try { await this.apiGateway.send(new DeleteApiMappingCommand({ - ApiMappingId: domain.apiMapping.apiMappingId, - DomainName: domain.givenDomainName, - })); + 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}` - ); + throw new Error( + `V2 - Unable to remove base path mapping for '${domain.givenDomainName}':\n${err.message}` + ); } } } diff --git a/src/aws/cloud-formation-wrapper.ts b/src/aws/cloud-formation-wrapper.ts index 8593222b..f6014de9 100644 --- a/src/aws/cloud-formation-wrapper.ts +++ b/src/aws/cloud-formation-wrapper.ts @@ -25,14 +25,12 @@ class CloudFormationWrapper { 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; + 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() + credentials, + region: Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() }); } @@ -42,7 +40,7 @@ class CloudFormationWrapper { public async findApiId(apiType: string): Promise { const configApiId = await this.getConfigId(apiType); if (configApiId) { - return configApiId; + return configApiId; } return await this.getStackApiId(apiType); @@ -57,11 +55,11 @@ class CloudFormationWrapper { const apiGatewayValue = apiGateway[apiIdKey]; if (apiGatewayValue) { - if (typeof apiGatewayValue === "string") { - return apiGatewayValue; - } + if (typeof apiGatewayValue === "string") { + return apiGatewayValue; + } - return await this.getCloudformationId(apiGatewayValue, apiType); + return await this.getCloudformationId(apiGatewayValue, apiType); } return null; @@ -71,22 +69,22 @@ class CloudFormationWrapper { // 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 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; - } + try { + return await this.getStackApiId(apiType, ref); + } catch (error) { + Logging.logWarning(`Unable to get ref ${ref} value.\n ${error.message}`); + return null; + } } // log warning not supported restApiId @@ -100,20 +98,20 @@ class CloudFormationWrapper { */ public async getStackApiId(apiType: string, logicalResourceId: string = null): Promise { if (!logicalResourceId) { - logicalResourceId = Globals.CFResourceIds[apiType]; + logicalResourceId = Globals.CFResourceIds[apiType]; } let response; try { - // trying to get information for specified stack name - response = await this.getStack(logicalResourceId, this.stackName); + // 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); + // in case error trying to get information from some of nested stacks + response = await this.getNestedStack(logicalResourceId, this.stackName); } if (!response) { - throw new Error(`Failed to find a stack ${this.stackName}\n`); + throw new Error(`Failed to find a stack ${this.stackName}\n`); } const apiId = response.StackResourceDetail.PhysicalResourceId; @@ -136,9 +134,7 @@ class CloudFormationWrapper { new ListExportsCommand({}) ); // filter Exports by names which we need - const filteredExports = exports.filter( - (item) => names.indexOf(item.Name) !== -1 - ); + 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}), {}); @@ -149,12 +145,12 @@ class CloudFormationWrapper { */ public async getStack(logicalResourceId: string, stackName: string): Promise { try { - return await this.cloudFormation.send( - new DescribeStackResourceCommand({ - LogicalResourceId: logicalResourceId, - StackName: stackName, - }) - ); + 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`); } @@ -178,23 +174,23 @@ class CloudFormationWrapper { const filteredStackNames = stacks .reduce((acc, stack) => { if (!stack.RootId) { - return acc; + return acc; } const match = stack.RootId.match(regex); if (match) { - acc.push(stack.StackName); + 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); - } + 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; } diff --git a/src/aws/route53-wrapper.ts b/src/aws/route53-wrapper.ts index f3d9526e..b8cb19be 100644 --- a/src/aws/route53-wrapper.ts +++ b/src/aws/route53-wrapper.ts @@ -17,16 +17,16 @@ class Route53Wrapper { constructor(credentials?: any, region?: string) { // not null and not undefined if (credentials) { - this.route53 = new Route53Client({ - credentials, - region: region || Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy() - }); + this.route53 = new Route53Client({ + credentials, + region: region || Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() + }); } else { - this.route53 = new Route53Client({ - region: Globals.getRegion(), - retryStrategy: Globals.getRetryStrategy() - }); + this.route53 = new Route53Client({ + region: Globals.getRegion(), + retryStrategy: Globals.getRetryStrategy() + }); } } @@ -35,44 +35,44 @@ class Route53Wrapper { */ public async getRoute53HostedZoneId(domain: DomainConfig, isHostedZonePrivate?: boolean): Promise { if (domain.hostedZoneId) { - Logging.logInfo(`Selected specific hostedZoneId ${domain.hostedZoneId}`); - return 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 zoneTypeString = isHostedZonePrivate ? "private" : "public"; + Logging.logInfo(`Filtering to only ${zoneTypeString} zones.`); } let hostedZones = []; try { - hostedZones = await getAWSPagedResults( - this.route53, - "HostedZones", - "Marker", - "NextMarker", - new ListHostedZonesCommand({}) - ); + hostedZones = await getAWSPagedResults( + this.route53, + "HostedZones", + "Marker", + "NextMarker", + new ListHostedZonesCommand({}) + ); } catch (err) { throw new Error(`Unable to list hosted zones in Route53.\n${err.message}`); } const targetHostedZone = hostedZones - .filter((hostedZone) => { - return !isPrivateDefined || isHostedZonePrivate === hostedZone.Config.PrivateZone; - }) - .filter((hostedZone) => { - const hostedZoneName = hostedZone.Name.replace(/\.$/, ""); - return domain.givenDomainName.endsWith(hostedZoneName); - }) - .sort((zone1, zone2) => zone2.Name.length - zone1.Name.length) - .shift(); + .filter((hostedZone) => { + return !isPrivateDefined || isHostedZonePrivate === hostedZone.Config.PrivateZone; + }) + .filter((hostedZone) => { + const hostedZoneName = hostedZone.Name.replace(/\.$/, ""); + return domain.givenDomainName.endsWith(hostedZoneName); + }) + .sort((zone1, zone2) => zone2.Name.length - zone1.Name.length) + .shift(); if (targetHostedZone) { - return targetHostedZone.Id.replace("/hostedzone/", ""); + return targetHostedZone.Id.replace("/hostedzone/", ""); } else { - throw new Error(`Could not find hosted zone '${domain.givenDomainName}'`); + throw new Error(`Could not find hosted zone '${domain.givenDomainName}'`); } } @@ -91,69 +91,69 @@ class Route53Wrapper { const route53Params = domain.route53Params; const route53healthCheck = route53Params.healthCheckId ? {HealthCheckId: route53Params.healthCheckId} : {}; const domainInfo = domain.domainInfo ?? { - domainName: domain.givenDomainName, - hostedZoneId: route53HostedZoneId, + domainName: domain.givenDomainName, + hostedZoneId: route53HostedZoneId, }; let routingOptions = {}; if (route53Params.routingPolicy === Globals.routingPolicies.latency) { - routingOptions = { - Region: await this.route53.config.region(), - SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, - ...route53healthCheck, - }; + routingOptions = { + Region: await this.route53.config.region(), + SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, + ...route53healthCheck, + }; } if (route53Params.routingPolicy === Globals.routingPolicies.weighted) { - routingOptions = { - Weight: route53Params.weight, - SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, - ...route53healthCheck, - }; + routingOptions = { + Weight: route53Params.weight, + SetIdentifier: route53Params.setIdentifier ?? domainInfo.domainName, + ...route53healthCheck, + }; } let hostedZoneIds: string[]; if (domain.splitHorizonDns) { - hostedZoneIds = await Promise.all([ - this.getRoute53HostedZoneId(domain, false), - this.getRoute53HostedZoneId(domain, true), - ]); + hostedZoneIds = await Promise.all([ + this.getRoute53HostedZoneId(domain, false), + this.getRoute53HostedZoneId(domain, true), + ]); } else { - hostedZoneIds = [route53HostedZoneId]; + hostedZoneIds = [route53HostedZoneId]; } const recordsToCreate = domain.createRoute53IPv6Record ? ["A", "AAAA"] : ["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, - }, - })); + const changes = recordsToCreate.map((Type) => ({ + Action: action, + ResourceRecordSet: { + AliasTarget: { + DNSName: domainInfo.domainName, + EvaluateTargetHealth: false, + HostedZoneId: domainInfo.hostedZoneId, + }, + 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}` - ); - } + 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}` + ); + } } } } diff --git a/src/utils.ts b/src/utils.ts index 3cfbc7a1..eec8f3ba 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -23,17 +23,17 @@ async function sleep(seconds: number) { */ function evaluateBoolean(value: any, defaultValue: boolean): boolean { if (value === undefined) { - return defaultValue; + return defaultValue; } const s = value.toString().toLowerCase().trim(); const trueValues = ["true", "1"]; const falseValues = ["false", "0"]; if (trueValues.indexOf(s) >= 0) { - return true; + return true; } if (falseValues.indexOf(s) >= 0) { - return false; + return false; } throw new Error(`${Globals.pluginName}: Ambiguous boolean config: "${value}"`); } From 1a0e72c25f1dff533682d8d37417f89a2372e3fc Mon Sep 17 00:00:00 2001 From: sromic Date: Fri, 11 Aug 2023 11:09:41 +0200 Subject: [PATCH 4/7] CHANGE - code style formatting revert --- src/aws/acm-wrapper.ts | 62 +++++++++++++++--------------- src/aws/api-gateway-v1-wrapper.ts | 20 +++++----- src/aws/api-gateway-v2-wrapper.ts | 20 +++++----- src/aws/cloud-formation-wrapper.ts | 2 +- 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/aws/acm-wrapper.ts b/src/aws/acm-wrapper.ts index 5710da43..12b11a28 100644 --- a/src/aws/acm-wrapper.ts +++ b/src/aws/acm-wrapper.ts @@ -28,21 +28,21 @@ class ACMWrapper { 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 = this.getCertArnByDomainName(certificates, certificateName); - } + 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 = this.getCertArnByDomainName(certificates, certificateName); + } } catch (err) { throw Error(`Could not search certificates in Certificate Manager.\n${err.message}`); } @@ -65,23 +65,23 @@ class ACMWrapper { 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; + 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; } diff --git a/src/aws/api-gateway-v1-wrapper.ts b/src/aws/api-gateway-v1-wrapper.ts index d56172fa..da9c182f 100644 --- a/src/aws/api-gateway-v1-wrapper.ts +++ b/src/aws/api-gateway-v1-wrapper.ts @@ -60,9 +60,9 @@ class APIGatewayV1Wrapper extends APIGatewayBase { truststoreUri: domain.tlsTruststoreUri }; - if (domain.tlsTruststoreVersion) { - params.mutualTlsAuthentication.truststoreVersion = domain.tlsTruststoreVersion; - } + if (domain.tlsTruststoreVersion) { + params.mutualTlsAuthentication.truststoreVersion = domain.tlsTruststoreVersion; + } } } @@ -82,16 +82,16 @@ class APIGatewayV1Wrapper extends APIGatewayBase { // Make API call try { const domainInfo: GetDomainNameCommandOutput = await this.apiGateway.send( - new GetDomainNameCommand({ - domainName: domain.givenDomainName, - }) + 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}` - ); + throw new Error( + `V1 - Unable to fetch information about '${domain.givenDomainName}':\n${err.message}` + ); } Logging.logWarning(`V1 - '${domain.givenDomainName}' does not exist.`); } @@ -158,7 +158,7 @@ class APIGatewayV1Wrapper extends APIGatewayBase { path: "/basePath", value: domain.basePath, }] - } + } )); Logging.logInfo(`V1 - Updated API mapping from '${domain.apiMapping.basePath}' to '${domain.basePath}' for '${domain.givenDomainName}'`); diff --git a/src/aws/api-gateway-v2-wrapper.ts b/src/aws/api-gateway-v2-wrapper.ts index 310a4c8d..f3bd0eb2 100644 --- a/src/aws/api-gateway-v2-wrapper.ts +++ b/src/aws/api-gateway-v2-wrapper.ts @@ -47,9 +47,9 @@ class APIGatewayV2Wrapper extends APIGatewayBase { const params: any = { DomainName: domain.givenDomainName, DomainNameConfigurations: [{ - CertificateArn: domain.certificateArn, - EndpointType: domain.endpointType, - SecurityPolicy: domain.securityPolicy, + CertificateArn: domain.certificateArn, + EndpointType: domain.endpointType, + SecurityPolicy: domain.securityPolicy, }], Tags: providerTags }; @@ -59,7 +59,7 @@ class APIGatewayV2Wrapper extends APIGatewayBase { params.MutualTlsAuthentication = { TruststoreUri: domain.tlsTruststoreUri }; - + if (domain.tlsTruststoreVersion) { params.MutualTlsAuthentication.TruststoreVersion = domain.tlsTruststoreVersion; } @@ -85,9 +85,9 @@ class APIGatewayV2Wrapper extends APIGatewayBase { // Make API call try { const domainInfo: GetDomainNameCommandOutput = await this.apiGateway.send( - new GetDomainNameCommand({ - DomainName: domain.givenDomainName - }) + new GetDomainNameCommand({ + DomainName: domain.givenDomainName + }) ); return new DomainInfo(domainInfo); } catch (err) { @@ -108,9 +108,9 @@ class APIGatewayV2Wrapper extends APIGatewayBase { // Make API call try { await this.apiGateway.send( - new DeleteDomainNameCommand({ - DomainName: domain.givenDomainName, - }) + new DeleteDomainNameCommand({ + DomainName: domain.givenDomainName, + }) ); } catch (err) { throw new Error( diff --git a/src/aws/cloud-formation-wrapper.ts b/src/aws/cloud-formation-wrapper.ts index f6014de9..180d0113 100644 --- a/src/aws/cloud-formation-wrapper.ts +++ b/src/aws/cloud-formation-wrapper.ts @@ -113,7 +113,7 @@ class CloudFormationWrapper { if (!response) { throw new Error(`Failed to find a stack ${this.stackName}\n`); } - + const apiId = response.StackResourceDetail.PhysicalResourceId; if (!apiId) { throw new Error(`No ApiId associated with CloudFormation stack ${this.stackName}`); From 39f5ea37cfa536c2395001e1f0af872a0e706e07 Mon Sep 17 00:00:00 2001 From: sromic Date: Fri, 11 Aug 2023 11:10:50 +0200 Subject: [PATCH 5/7] CHANGE - fixed code style formatting --- src/aws/api-gateway-v1-wrapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aws/api-gateway-v1-wrapper.ts b/src/aws/api-gateway-v1-wrapper.ts index da9c182f..df83d151 100644 --- a/src/aws/api-gateway-v1-wrapper.ts +++ b/src/aws/api-gateway-v1-wrapper.ts @@ -133,7 +133,7 @@ class APIGatewayV1Wrapper extends APIGatewayBase { "position", "position", new GetBasePathMappingsCommand({ - domainName: domain.givenDomainName, + domainName: domain.givenDomainName, }) ); return items.map((item) => { From 63ed97f88fc49669b02569f1a1303eb8faaa456d Mon Sep 17 00:00:00 2001 From: sromic Date: Fri, 11 Aug 2023 17:06:33 +0200 Subject: [PATCH 6/7] CHANGE - added a little type lvl restrictions to aws utility --- src/utils.ts | 3 ++- test/unit-tests/utils.test.ts | 26 +++++++++++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index eec8f3ba..97ace62b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import { Client, Command } from "@smithy/smithy-client"; +import { MetadataBearer } from "@smithy/types"; import Globals from "./globals"; /** @@ -47,7 +48,7 @@ function evaluateBoolean(value: any, defaultValue: boolean): boolean { * @param nextRequestTokenKey - The response key name that has the next paging token value * @param params - Parameters to send in the request */ -async function getAWSPagedResults( +async function getAWSPagedResults( client: Client, resultsKey: keyof ClientOutputCommand, nextTokenKey: keyof ClientInputCommand, diff --git a/test/unit-tests/utils.test.ts b/test/unit-tests/utils.test.ts index ee4260e7..37dc11a1 100644 --- a/test/unit-tests/utils.test.ts +++ b/test/unit-tests/utils.test.ts @@ -1,23 +1,35 @@ import { expect } from "./base"; import { getAWSPagedResults } from "../../src/utils"; import { mockClient } from "aws-sdk-client-mock"; -import { ACMClient, ListCertificatesCommand } from "@aws-sdk/client-acm"; +import { ACMClient, CertificateSummary, ListCertificatesCommand, ListCertificatesCommandInput, ListCertificatesCommandOutput } from "@aws-sdk/client-acm"; import { Client } from "@aws-sdk/smithy-client"; import { APIGatewayClient, + BasePathMapping, GetBasePathMappingsCommand, + GetBasePathMappingsCommandInput, + GetBasePathMappingsCommandOutput } from "@aws-sdk/client-api-gateway"; import { ApiGatewayV2Client, + ApiMapping, GetApiMappingsCommand, + GetApiMappingsCommandInput, + GetApiMappingsCommandOutput } from "@aws-sdk/client-apigatewayv2"; import { CloudFormationClient, + Export, ListExportsCommand, + ListExportsCommandInput, + ListExportsCommandOutput } from "@aws-sdk/client-cloudformation"; import { + HostedZone, ListHostedZonesCommand, - Route53Client, + ListHostedZonesCommandInput, + ListHostedZonesCommandOutput, + Route53Client } from "@aws-sdk/client-route-53"; describe("Utils checks", () => { @@ -48,7 +60,7 @@ describe("Utils checks", () => { const certStatuses = ["PENDING_VALIDATION", "ISSUED", "INACTIVE"]; - const certs = await getAWSPagedResults( + const certs = await getAWSPagedResults( ACMCMock as unknown as Client, "CertificateSummaryList", "NextToken", @@ -84,7 +96,7 @@ describe("Utils checks", () => { ], }); - const items = await getAWSPagedResults( + const items = await getAWSPagedResults( APIGatewayCMock as unknown as Client, "items", "position", @@ -141,7 +153,7 @@ describe("Utils checks", () => { ], }); - const items = await getAWSPagedResults( + const items = await getAWSPagedResults( APIGatewayV2CMock as unknown as Client, "Items", "NextToken", @@ -181,7 +193,7 @@ describe("Utils checks", () => { ], }); - const items = await getAWSPagedResults( + const items = await getAWSPagedResults( CloudFormationCMock as unknown as Client, "Exports", "NextToken", @@ -227,7 +239,7 @@ describe("Utils checks", () => { ], }); - const items = await getAWSPagedResults( + const items = await getAWSPagedResults( Route53CMock as unknown as Client, "HostedZones", "Marker", From 91c883a70898d391dba44f3d6334e6af7b8d7b09 Mon Sep 17 00:00:00 2001 From: sromic Date: Mon, 14 Aug 2023 16:27:55 +0200 Subject: [PATCH 7/7] CHANGE - moved tests to appropriate files --- test/unit-tests/aws/acm-wrapper.test.ts | 33 +++ .../aws/api-gateway-v1-wrapper.test.ts | 38 +++ .../aws/api-gateway-v2-wrapper.test.ts | 35 +++ .../aws/cloud-formation-wrapper.test.ts | 98 +++++++ test/unit-tests/aws/route53-wrapper.test.ts | 83 +++++- test/unit-tests/utils.test.ts | 253 ------------------ 6 files changed, 286 insertions(+), 254 deletions(-) delete mode 100644 test/unit-tests/utils.test.ts diff --git a/test/unit-tests/aws/acm-wrapper.test.ts b/test/unit-tests/aws/acm-wrapper.test.ts index a9e39ed0..27e08e06 100644 --- a/test/unit-tests/aws/acm-wrapper.test.ts +++ b/test/unit-tests/aws/acm-wrapper.test.ts @@ -60,6 +60,39 @@ describe("ACM Wrapper checks", () => { const actualResult = await acmWrapper.getCertArn(dc); expect(actualResult).to.equal(testCertificateArnByDomain); }); + + it("getCertArn by domain name by getting all paginated certificates from AWS", async () => { + const ACMCMock = mockClient(ACMClient); + ACMCMock.on(ListCertificatesCommand) + .resolvesOnce({ + CertificateSummaryList: [ + { + CertificateArn: "test_domain_arn", + DomainName: "test_domain", + Status: "ISSUED", + }, + ], + NextToken: 'NextToken', + }) + .resolves({ + CertificateSummaryList: [ + { + CertificateArn: "test_domain_arn2", + DomainName: "test_domain2", + Status: "ISSUED", + }, + ], + }); + + const acmWrapper = new ACMWrapper(null, Globals.endpointTypes.regional); + const dc = new DomainConfig(getDomainConfig({ + domainName: "test_domain", + })); + + const actualResult = await acmWrapper.getCertArn(dc); + expect(actualResult).to.equal(testCertificateArnByDomain); + expect(ACMCMock.calls().length).to.equal(2); + }); it("empty getCertArn by certificate name", async () => { const ACMCMock = mockClient(ACMClient); diff --git a/test/unit-tests/aws/api-gateway-v1-wrapper.test.ts b/test/unit-tests/aws/api-gateway-v1-wrapper.test.ts index d5f85b41..22635f81 100644 --- a/test/unit-tests/aws/api-gateway-v1-wrapper.test.ts +++ b/test/unit-tests/aws/api-gateway-v1-wrapper.test.ts @@ -361,6 +361,44 @@ describe("API Gateway V1 wrapper checks", () => { expect(commandCalls.length).to.equal(1); }); + + it("get all base path mappings", async () => { + const APIGatewayCMock = mockClient(APIGatewayClient); + APIGatewayCMock.on(GetBasePathMappingsCommand) + .resolvesOnce({ + items: [ + { + restApiId: "test_rest_api_id", + basePath: "test", + stage: "test" + }, + ], + position: "position", + }) + .resolves({ + items: [ + { + restApiId: "test_rest_api_id2", + basePath: "test2", + stage: "test", + }, + ], + }); + + const apiGatewayV1Wrapper = new APIGatewayV1Wrapper(); + const dc = new DomainConfig(getDomainConfig({ + domainName: "test_domain" + })); + + const actualResult = await apiGatewayV1Wrapper.getBasePathMappings(dc); + const expectedResult = [ + new ApiGatewayMap("test_rest_api_id", "test", "test", null), + new ApiGatewayMap("test_rest_api_id2", "test2", "test", null), + ] + + expect(actualResult).to.eql(expectedResult); + expect(APIGatewayCMock.calls().length).to.equal(2); + }); it("get base path mapping failure", async () => { const APIGatewayMock = mockClient(APIGatewayClient); diff --git a/test/unit-tests/aws/api-gateway-v2-wrapper.test.ts b/test/unit-tests/aws/api-gateway-v2-wrapper.test.ts index 5932e13d..45af2675 100644 --- a/test/unit-tests/aws/api-gateway-v2-wrapper.test.ts +++ b/test/unit-tests/aws/api-gateway-v2-wrapper.test.ts @@ -395,6 +395,41 @@ describe("API Gateway V2 wrapper checks", () => { expect(commandCalls.length).to.equal(1); }); + + it("get all base path mappings", async () => { + const APIGatewayMock = mockClient(ApiGatewayV2Client); + APIGatewayMock.on(GetApiMappingsCommand).resolvesOnce({ + Items: [{ + ApiId: "test_rest_api_id", + ApiMappingKey: "test", + Stage: "test", + ApiMappingId: "test_id" + }], + NextToken: "NextToken" + }) + .resolves({ + Items: [{ + ApiId: "test_rest_api_id2", + ApiMappingKey: "test2", + Stage: "test", + ApiMappingId: "test_id2" + }] + }); + + const apiGatewayV2Wrapper = new APIGatewayV2Wrapper(); + const dc = new DomainConfig(getDomainConfig({ + domainName: "test_domain" + })); + + const actualResult = await apiGatewayV2Wrapper.getBasePathMappings(dc); + const expectedResult = [ + new ApiGatewayMap("test_rest_api_id", "test", "test", "test_id"), + new ApiGatewayMap("test_rest_api_id2", "test2", "test", "test_id2") + ] + + expect(actualResult).to.eql(expectedResult); + expect(APIGatewayMock.calls().length).to.equal(2); + }); it("get base path mapping failure", async () => { const APIGatewayMock = mockClient(ApiGatewayV2Client); diff --git a/test/unit-tests/aws/cloud-formation-wrapper.test.ts b/test/unit-tests/aws/cloud-formation-wrapper.test.ts index a3a18882..d08330bd 100644 --- a/test/unit-tests/aws/cloud-formation-wrapper.test.ts +++ b/test/unit-tests/aws/cloud-formation-wrapper.test.ts @@ -56,6 +56,38 @@ describe("Cloud Formation wrapper checks", () => { const commandCalls = CloudFormationMock.commandCalls(ListExportsCommand, expectedParams, true); expect(commandCalls.length).to.equal(1); }); + + it("findApiId for the rest api type via Fn::ImportValue paginated", async () => { + const fnImportValue = "test-value"; + const CloudFormationMock = mockClient(CloudFormationClient); + CloudFormationMock.on(ListExportsCommand).resolvesOnce({ + Exports: [ + {Name: "test-name", Value: fnImportValue}, + {Name: "dummy-name", Value: "dummy-value"}, + ], + NextToken: "NextToken" + }) + .resolves({ + Exports: [ + {Name: "test-name2", Value: "test-name2"}, + {Name: "dummy-name2", Value: "dummy-value2"}, + ] + }); + + const cloudFormationWrapper = new CloudFormationWrapper(); + Globals.serverless.service.provider.apiGateway.restApiId = { + [Globals.CFFuncNames.fnImport]: "test-name" + }; + + const actualResult = await cloudFormationWrapper.findApiId(Globals.apiTypes.rest) + expect(actualResult).to.equal(fnImportValue); + + const expectedParams = { + NextToken: "NextToken" + }; + const commandCalls = CloudFormationMock.commandCalls(ListExportsCommand, expectedParams, true); + expect(commandCalls.length).to.equal(2); + }); it("findApiId for the rest api type via Fn::ImportValue not found", async () => { const fnImportValue = "test-value"; @@ -265,6 +297,72 @@ describe("Cloud Formation wrapper checks", () => { const allCommandCalls = CloudFormationMock.commandCalls(DescribeStackResourceCommand); expect(allCommandCalls.length).to.equal(2); }); + + it("findApiId for the rest api type with paginated nested stacks", async () => { + const physicalResourceId = "test_rest_api_id"; + const nestedStackName = "custom-stage-name-NestedStackTwo-U89W84TQIHJK"; + const CloudFormationMock = mockClient(CloudFormationClient); + CloudFormationMock.on(DescribeStackResourceCommand).rejectsOnce() + .resolves({ + StackResourceDetail: { + LogicalResourceId: Globals.CFResourceIds[Globals.apiTypes.rest], + PhysicalResourceId: physicalResourceId, + ResourceType: "", + LastUpdatedTimestamp: undefined, + ResourceStatus: ResourceStatus.CREATE_COMPLETE, + }, + }); + CloudFormationMock.on(DescribeStacksCommand).resolvesOnce({ + Stacks: [ + { + StackName: "custom-stage-name-NestedStackOne-U89W84TQIHJK", + RootId: "arn:aws:cloudformation:us-east-1:000000000000:stack/dummy-name/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + CreationTime: null, + StackStatus: StackStatus.CREATE_COMPLETE + }, + { + StackName: nestedStackName, + RootId: `arn:aws:cloudformation:us-east-1:000000000000:stack/${Globals.serverless.service.provider.stackName}/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, + CreationTime: null, + StackStatus: StackStatus.CREATE_COMPLETE + }, + { + StackName: "outside-stack-NestedStackZERO-U89W84TQIHJK", + RootId: null, + CreationTime: null, + StackStatus: StackStatus.CREATE_COMPLETE + }, + ], + NextToken: "NextToken" + }) + .resolves({ + Stacks: [ + { + StackName: "custom-stage-name-NestedStackOne-U89W84TQ1235", + RootId: "arn:aws:cloudformation:us-east-1:000000000000:stack/dummy-name2/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + CreationTime: null, + StackStatus: StackStatus.CREATE_COMPLETE + } + ], + }); + + const actualResult = await new CloudFormationWrapper().findApiId(Globals.apiTypes.rest) + expect(actualResult).to.equal(physicalResourceId); + + const expectedParams = { + LogicalResourceId: Globals.CFResourceIds[Globals.apiTypes.rest], + StackName: nestedStackName, + }; + + const commandCalls = CloudFormationMock.commandCalls(DescribeStackResourceCommand, expectedParams, true); + expect(commandCalls.length).to.equal(1); + + const allCommandCalls = CloudFormationMock.commandCalls(DescribeStackResourceCommand); + expect(allCommandCalls.length).to.equal(2); + + const describeStacksCommandCalls = CloudFormationMock.commandCalls(DescribeStacksCommand); + expect(describeStacksCommandCalls.length).to.equal(2); + }); it("findApiId for the rest api type with nested stacks failure", async () => { const nestedStackName = "custom-stage-name-NestedStackTwo-U89W84TQIHJK"; diff --git a/test/unit-tests/aws/route53-wrapper.test.ts b/test/unit-tests/aws/route53-wrapper.test.ts index ca4a4f18..576892a7 100644 --- a/test/unit-tests/aws/route53-wrapper.test.ts +++ b/test/unit-tests/aws/route53-wrapper.test.ts @@ -70,6 +70,87 @@ describe("Route53 wrapper checks", () => { actualId = await new Route53Wrapper().getRoute53HostedZoneId(dc); expect(actualId).to.equal(dc.hostedZoneId); }); + + it("get route53 hosted zone id paginated", async () => { + const testId = "test_host_id" + const Route53Mock = mockClient(Route53Client); + Route53Mock.on(ListHostedZonesCommand).resolvesOnce({ + HostedZones: [ + { + CallerReference: "1", + Config: {PrivateZone: false}, + Id: testId, + Name: "test_domain", + }, { + CallerReference: "2", + Config: {PrivateZone: false}, + Id: testId, + Name: "dummy_test_domain", + }, { + CallerReference: "3", + Config: {PrivateZone: false}, + Id: testId, + Name: "domain", + } + ], + NextMarker: "NextMarker" + }) + .resolvesOnce({ + HostedZones: [ + { + CallerReference: "4", + Config: {PrivateZone: false}, + Id: testId, + Name: "test_domain2", + }, { + CallerReference: "5", + Config: {PrivateZone: false}, + Id: testId, + Name: "dummy_test_domain2", + }, { + CallerReference: "6", + Config: {PrivateZone: false}, + Id: testId, + Name: "domain2", + } + ], + NextMarker: "NextMarker" + }) + .resolves({ + HostedZones: [ + { + CallerReference: "7", + Config: {PrivateZone: false}, + Id: testId, + Name: "test_domain3", + }, { + CallerReference: "8", + Config: {PrivateZone: false}, + Id: testId, + Name: "dummy_test_domain3", + }, { + CallerReference: "9", + Config: {PrivateZone: false}, + Id: testId, + Name: "domain3", + } + ] + }); + + const dc = new DomainConfig(getDomainConfig({ + domainName: "test_domain" + })); + + let actualId = await new Route53Wrapper().getRoute53HostedZoneId(dc); + expect(actualId).to.equal(testId); + + const commandCalls = Route53Mock.commandCalls(ListHostedZonesCommand, {}); + expect(commandCalls.length).to.equal(3); + + dc.hostedZoneId = "test_id" + actualId = await new Route53Wrapper().getRoute53HostedZoneId(dc); + expect(actualId).to.equal(dc.hostedZoneId); + }); it("get route53 hosted zone id public", async () => { const testId = "test_host_id" @@ -101,7 +182,7 @@ describe("Route53 wrapper checks", () => { expect(commandCalls.length).to.equal(1); }); - it("get route53 hosted zone id privet", async () => { + it("get route53 hosted zone id private", async () => { const testId = "test_host_id" const Route53Mock = mockClient(Route53Client); Route53Mock.on(ListHostedZonesCommand).resolves({ diff --git a/test/unit-tests/utils.test.ts b/test/unit-tests/utils.test.ts deleted file mode 100644 index 37dc11a1..00000000 --- a/test/unit-tests/utils.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { expect } from "./base"; -import { getAWSPagedResults } from "../../src/utils"; -import { mockClient } from "aws-sdk-client-mock"; -import { ACMClient, CertificateSummary, ListCertificatesCommand, ListCertificatesCommandInput, ListCertificatesCommandOutput } from "@aws-sdk/client-acm"; -import { Client } from "@aws-sdk/smithy-client"; -import { - APIGatewayClient, - BasePathMapping, - GetBasePathMappingsCommand, - GetBasePathMappingsCommandInput, - GetBasePathMappingsCommandOutput -} from "@aws-sdk/client-api-gateway"; -import { - ApiGatewayV2Client, - ApiMapping, - GetApiMappingsCommand, - GetApiMappingsCommandInput, - GetApiMappingsCommandOutput -} from "@aws-sdk/client-apigatewayv2"; -import { - CloudFormationClient, - Export, - ListExportsCommand, - ListExportsCommandInput, - ListExportsCommandOutput -} from "@aws-sdk/client-cloudformation"; -import { - HostedZone, - ListHostedZonesCommand, - ListHostedZonesCommandInput, - ListHostedZonesCommandOutput, - Route53Client -} from "@aws-sdk/client-route-53"; - -describe("Utils checks", () => { - describe("acm-wrapper", () => { - it("get all certificates", async () => { - const ACMCMock = mockClient(ACMClient); - ACMCMock.on(ListCertificatesCommand) - .resolvesOnce({ - CertificateSummaryList: [ - { - CertificateArn: "test_certificate_arn", - DomainName: "test_domain", - Status: "ISSUED", - }, - ], - NextToken: - '{"CertificateArn": "test_certificate_arn2","DomainName": "test_domain2","Status": "ISSUED"}', - }) - .resolves({ - CertificateSummaryList: [ - { - CertificateArn: "test_certificate_arn2", - DomainName: "test_domain2", - Status: "ISSUED", - }, - ], - }); - - const certStatuses = ["PENDING_VALIDATION", "ISSUED", "INACTIVE"]; - - const certs = await getAWSPagedResults( - ACMCMock as unknown as Client, - "CertificateSummaryList", - "NextToken", - "NextToken", - new ListCertificatesCommand({ CertificateStatuses: certStatuses }) - ); - expect(certs.length).to.equal(2); - expect(ACMCMock.calls().length).to.equal(2); - }); - }); - - describe("api-gateway-v1-wrapper", () => { - it("get all base path mappings", async () => { - const APIGatewayCMock = mockClient(APIGatewayClient); - APIGatewayCMock.on(GetBasePathMappingsCommand) - .resolvesOnce({ - items: [ - { - restApiId: "1", - basePath: "test_domain", - stage: "mock", - }, - ], - position: "position", - }) - .resolves({ - items: [ - { - restApiId: "2", - basePath: "test_domain2", - stage: "mock", - }, - ], - }); - - const items = await getAWSPagedResults( - APIGatewayCMock as unknown as Client, - "items", - "position", - "position", - new GetBasePathMappingsCommand({ - domainName: "domain", - }) - ); - expect(items.length).to.equal(2); - expect(APIGatewayCMock.calls().length).to.equal(2); - }); - }); - - describe("api-gateway-v2-wrapper", () => { - it("get all api mappings", async () => { - const APIGatewayV2CMock = mockClient(ApiGatewayV2Client); - APIGatewayV2CMock.on(GetApiMappingsCommand) - .resolvesOnce({ - Items: [ - { - ApiId: "ApiId", - ApiMappingKey: "ApiMappingKey", - Stage: "mock", - ApiMappingId: "ApiMappingId", - }, - ], - NextToken: 'NextToken', - }) - .resolvesOnce({ - Items: [ - { - ApiId: "ApiId4", - ApiMappingKey: "ApiMappingKey4", - Stage: "mock", - ApiMappingId: "ApiMappingId4", - }, - ], - NextToken: 'NextToken', - }) - .resolves({ - Items: [ - { - ApiId: "ApiId2", - ApiMappingKey: "ApiMappingKey2", - Stage: "mock", - ApiMappingId: "ApiMappingId2", - }, - { - ApiId: "ApiId3", - ApiMappingKey: "ApiMappingKey3", - Stage: "mock", - ApiMappingId: "ApiMappingId3", - }, - ], - }); - - const items = await getAWSPagedResults( - APIGatewayV2CMock as unknown as Client, - "Items", - "NextToken", - "NextToken", - new GetApiMappingsCommand({ - DomainName: "domain", - }) - ); - expect(items.length).to.equal(4); - expect(APIGatewayV2CMock.calls().length).to.equal(3); - }); - }); - - describe("cloud-formation-wrapper", () => { - it("get all exports", async () => { - const CloudFormationCMock = mockClient(CloudFormationClient); - CloudFormationCMock.on(ListExportsCommand) - .resolvesOnce({ - Exports: [ - { - Name: "Name1", - }, - { - Name: "Name4", - }, - ], - NextToken: 'NextToken', - }) - .resolves({ - Exports: [ - { - Name: "Name2", - }, - { - Name: "Name3", - }, - ], - }); - - const items = await getAWSPagedResults( - CloudFormationCMock as unknown as Client, - "Exports", - "NextToken", - "NextToken", - new ListExportsCommand({}) - ); - expect(items.length).to.equal(4); - expect(CloudFormationCMock.calls().length).to.equal(2); - }); - }); - - describe("route53-wrapper", () => { - it("get all hosted zones", async () => { - const Route53CMock = mockClient(Route53Client); - Route53CMock.on(ListHostedZonesCommand) - .resolvesOnce({ - HostedZones: [ - { - Id: "Id1", - Name: "Name1", - CallerReference: "CallerReference1", - }, - { - Id: "Id2", - Name: "Name2", - CallerReference: "CallerReference2", - }, - ], - NextMarker: 'NextMarker', - }) - .resolves({ - HostedZones: [ - { - Id: "Id3", - Name: "Name3", - CallerReference: "CallerReference3", - }, - { - Id: "Id4", - Name: "Name4", - CallerReference: "CallerReference4", - }, - ], - }); - - const items = await getAWSPagedResults( - Route53CMock as unknown as Client, - "HostedZones", - "Marker", - "NextMarker", - new ListHostedZonesCommand({}) - ); - expect(items.length).to.equal(4); - expect(Route53CMock.calls().length).to.equal(2); - }); - }); -});