From d9a49f4cce417371061a376613d8fb9db8cab5bf Mon Sep 17 00:00:00 2001 From: Jason Venable Date: Sun, 16 Feb 2020 21:45:52 -0500 Subject: [PATCH 1/5] Add support for HTTP and WebSocket Domain Names --- DomainConfig.ts | 100 +++++ DomainInfo.ts | 18 +- Globals.ts | 23 ++ README.md | 56 ++- index.ts | 723 ++++++++++++++++++++-------------- package-lock.json | 105 +++-- test/unit-tests/index.test.ts | 614 +++++++++++++++++++++-------- types.ts | 2 + 8 files changed, 1155 insertions(+), 486 deletions(-) create mode 100644 DomainConfig.ts create mode 100644 Globals.ts diff --git a/DomainConfig.ts b/DomainConfig.ts new file mode 100644 index 00000000..5a67ed01 --- /dev/null +++ b/DomainConfig.ts @@ -0,0 +1,100 @@ +/** + * Wrapper class for Custom Domain information + */ + +import Globals from "./Globals"; +import DomainInfo = require("./DomainInfo"); +import * as AWS from "aws-sdk"; // imported for Types + +class DomainConfig { + + public givenDomainName: string; + public basePath: string | undefined; + public stage: string | undefined; + public certificateName: string | undefined; + public certificateArn: string | undefined; + public createRoute53Record: boolean | undefined; + public endpointType: string | undefined; + public apiType: string | undefined; + public hostedZoneId: string | undefined; + public hostedZonePrivate: boolean | undefined; + public enabled: boolean | string | undefined; + public securityPolicy: string | undefined; + + public domainInfo: DomainInfo | undefined; + public apiId: string | undefined; + public apiMapping: AWS.ApiGatewayV2.GetApiMappingResponse; + public allowPathMatching: boolean | false; + + constructor(config: any) { + + this.enabled = this.evaluateEnabled(config.enabled); + this.givenDomainName = config.domainName; + this.hostedZonePrivate = config.hostedZonePrivate; + this.certificateArn = config.certificateArn; + this.certificateName = config.certificateName; + this.createRoute53Record = config.createRoute53Record; + this.hostedZoneId = config.hostedZoneId; + this.hostedZonePrivate = config.hostedZonePrivate; + this.allowPathMatching = config.allowPathMatching; + + let basePath = config.basePath; + if (basePath == null || basePath.trim() === "") { + basePath = "(none)"; + } + this.basePath = basePath; + + let stage = config.stage; + if (typeof stage === "undefined") { + stage = Globals.options.stage || Globals.serverless.service.provider.stage; + } + this.stage = stage; + + const endpointTypeWithDefault = config.endpointType || Globals.endpointTypes.edge; + const endpointTypeToUse = Globals.endpointTypes[endpointTypeWithDefault.toLowerCase()]; + if (!endpointTypeToUse) { + throw new Error(`${endpointTypeWithDefault} is not supported endpointType, use edge or regional.`); + } + this.endpointType = endpointTypeToUse; + + const apiTypeWithDefault = config.apiType || Globals.apiTypes.rest; + const apiTypeToUse = Globals.apiTypes[apiTypeWithDefault.toLowerCase()]; + if (!apiTypeToUse) { + throw new Error(`${apiTypeWithDefault} is not supported api type, use REST, HTTP or WEBSOCKET.`); + } + this.apiType = apiTypeToUse; + + const securityPolicyDefault = config.securityPolicy || Globals.tlsVersions.tls_1_2; + const tlsVersionToUse = Globals.tlsVersions[securityPolicyDefault.toLowerCase()]; + if (!tlsVersionToUse) { + throw new Error(`${securityPolicyDefault} is not a supported securityPolicy, use tls_1_0 or tls_1_2.`); + } + this.securityPolicy = tlsVersionToUse; + } + + /** + * Determines whether this plug-in is enabled. + * + * This method reads the customDomain property "enabled" to see if this plug-in should be enabled. + * If the property's value is undefined, a default value of true is assumed (for backwards + * compatibility). + * If the property's value is provided, this should be boolean, otherwise an exception is thrown. + * If no customDomain object exists, an exception is thrown. + */ + public evaluateEnabled(enabled: any): boolean { + // const enabled = this.serverless.service.custom.customDomain.enabled; + if (enabled === undefined) { + return true; + } + if (typeof enabled === "boolean") { + return enabled; + } else if (typeof enabled === "string" && enabled === "true") { + return true; + } else if (typeof enabled === "string" && enabled === "false") { + return false; + } + throw new Error(`serverless-domain-manager: Ambiguous enablement boolean: "${enabled}"`); + } +} + +export = DomainConfig; diff --git a/DomainInfo.ts b/DomainInfo.ts index 113bb75d..7b40fc26 100644 --- a/DomainInfo.ts +++ b/DomainInfo.ts @@ -18,11 +18,19 @@ class DomainInfo { private defaultSecurityPolicy: string = "TLS_1_2"; constructor(data: any) { - this.domainName = data.distributionDomainName || data.regionalDomainName; - this.hostedZoneId = data.distributionHostedZoneId || - data.regionalHostedZoneId || - this.defaultHostedZoneId; - this.securityPolicy = data.securityPolicy || this.defaultSecurityPolicy; + this.domainName = data.distributionDomainName + || data.regionalDomainName + || data.DomainNameConfigurations && data.DomainNameConfigurations[0].ApiGatewayDomainName + || data.DomainName; + + this.hostedZoneId = data.distributionHostedZoneId + || data.regionalHostedZoneId + || data.DomainNameConfigurations && data.DomainNameConfigurations[0].HostedZoneId + || this.defaultHostedZoneId; + + this.securityPolicy = data.securityPolicy + || data.DomainNameConfigurations && data.DomainNameConfigurations[0].SecurityPolicy + || this.defaultSecurityPolicy; } } diff --git a/Globals.ts b/Globals.ts new file mode 100644 index 00000000..d95ee9de --- /dev/null +++ b/Globals.ts @@ -0,0 +1,23 @@ +import { ServerlessInstance, ServerlessOptions } from "./types"; + +export default class Globals { + + public static serverless: ServerlessInstance; + public static options: ServerlessOptions; + + public static endpointTypes = { + edge: "EDGE", + regional: "REGIONAL", + }; + + public static apiTypes = { + http: "HTTP", + rest: "REST", + websocket: "WEBSOCKET", + }; + + public static tlsVersions = { + tls_1_0: "TLS_1_0", + tls_1_2: "TLS_1_2", + }; +} diff --git a/README.md b/README.md index 70333413..a3b12207 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ plugins: - serverless-domain-manager ``` -Add the plugin configuration (example for `serverless.foo.com/api`). +Add the plugin configuration (example for `serverless.foo.com/api`). For a single domain and API type the following sturcture can be used. ```yaml custom: @@ -70,6 +70,38 @@ custom: createRoute53Record: true endpointType: 'regional' securityPolicy: tls_1_2 + apiType: rest +``` + +Multiple API types mapped to different domains can also be supported with the follow structure. The key is the API Gateway API type. + +```yaml +custom: + customDomain: + rest: + domainName: rest.serverless.foo.com + stage: ci + basePath: api + certificateName: '*.foo.com' + createRoute53Record: true + endpointType: 'regional' + securityPolicy: tls_1_2 + http: + domainName: http.serverless.foo.com + stage: ci + basePath: api + certificateName: '*.foo.com' + createRoute53Record: true + endpointType: 'regional' + securityPolicy: tls_1_2 + websocket: + domainName: ws.serverless.foo.com + stage: ci + basePath: api + certificateName: '*.foo.com' + createRoute53Record: true + endpointType: 'regional' + securityPolicy: tls_1_2 ``` | Parameter Name | Default Value | Description | @@ -81,10 +113,13 @@ custom: | certificateArn | `(none)` | The arn of a specific certificate from Certificate Manager to use with this API. | | createRoute53Record | `true` | Toggles whether or not the plugin will create an A Alias and AAAA Alias records in Route53 mapping the `domainName` to the generated distribution domain name. If false, does not create a record. | | endpointType | edge | Defines the endpoint type, accepts `regional` or `edge`. | +| apiType | rest | Defines the api type, accepts `rest`, `http` or `websocket`. | | hostedZoneId | | If hostedZoneId is set the route53 record set will be created in the matching zone, otherwise the hosted zone will be figured out from the domainName (hosted zone with matching domain). | | hostedZonePrivate | | If hostedZonePrivate is set to `true` then only private hosted zones will be used for route 53 records. If it is set to `false` then only public hosted zones will be used for route53 records. Setting this parameter is specially useful if you have multiple hosted zones with the same domain name (e.g. a public and a private one) | | enabled | true | Sometimes there are stages for which is not desired to have custom domain names. This flag allows the developer to disable the plugin for such cases. Accepts either `boolean` or `string` values and defaults to `true` for backwards compatibility. | securityPolicy | tls_1_2 | The security policy to apply to the custom domain name. Accepts `tls_1_0` or `tls_1_2`| +allowPathMatching | false | When updating an existing api mapping this will match on the basePath instead of the API ID to find existing mappings for an udpate. This should ony be used when changing API types. For example, migrating a REST API to an HTTP API. See Changing API Types for moe information. | + ## Running @@ -139,6 +174,25 @@ npm install ## Writing Integration Tests Unit tests are found in `test/unit-tests`. Integration tests are found in `test/integration-tests`. Each folder in `tests/integration-tests` contains the serverless-domain-manager configuration being tested. To create a new integration test, create a new folder for the `handler.js` and `serverless.yml` with the same naming convention and update `integration.test.js`. +## Changing API Types +AWS API Gateway has three different API types: REST, HTTP, and WebSocket. Special steps need to be taken when migrating from one api type to another. A common migration will be from a REST API to an HTTP API given the potential cost savings. Below are the steps required to change from REST to HTTP. A similar process can be applied for other API type migrations. + +**REST to HTTP** +1) Confirm the Domain name is a Regional domain name. Edge domains are not supported by AWS for HTTP APIs. See this [guide for migrating an edge-optimized custom domain name to regional]( +https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-regional-api-custom-domain-migrate.html). +2) Wait for all DNS changes to take effect/propagate and ensure all traffic is being routed to the regional domain name before proceeding. +3) Make sure you have setup new or modified existing routes to use [httpApi event](https://serverless.com/framework/docs/providers/aws/events/http-api) in your serverless.yml file. +4) Make the following changes to the `customDomain` properties in the serverless.yml confg: + ```yaml + endpointType: regional + apiType: http + allowPathMatching: true # Only for one deploy + ``` +5) Run `sls deploy` +6) Remove the `allowPathMatching` option, it should only be used once when migrating a base path from one API type to another. + +NOTE: Always test this process in a lower level staging or development environment before performing it in production. + # Known Issues * (5/23/2017) CloudFormation does not support changing the base path from empty to something or vice a versa. You must run `sls remove` to remove the base path mapping. diff --git a/index.ts b/index.ts index 6cf8a81d..f3b034ef 100644 --- a/index.ts +++ b/index.ts @@ -2,24 +2,17 @@ import chalk from "chalk"; import DomainInfo = require("./DomainInfo"); +import DomainConfig = require("./DomainConfig"); +import Globals from "./Globals"; import { ServerlessInstance, ServerlessOptions } from "./types"; -const endpointTypes = { - edge: "EDGE", - regional: "REGIONAL", -}; - -const tlsVersions = { - tls_1_0: "TLS_1_0", - tls_1_2: "TLS_1_2", -}; - const certStatuses = ["PENDING_VALIDATION", "ISSUED", "INACTIVE"]; class ServerlessCustomDomain { // AWS SDK resources public apigateway: any; + public apigatewayV2: any; public route53: any; public acm: any; public acmRegion: string; @@ -32,17 +25,14 @@ class ServerlessCustomDomain { public hooks: object; // Domain Manager specific properties - public enabled: boolean; - public givenDomainName: string; - public hostedZonePrivate: boolean; - public basePath: string; - private endpointType: string; - private stage: string; - private securityPolicy: string; + public domains: DomainConfig[] = []; constructor(serverless: ServerlessInstance, options: ServerlessOptions) { this.serverless = serverless; + Globals.serverless = serverless; + this.options = options; + Globals.options = options; this.commands = { create_domain: { @@ -61,12 +51,12 @@ class ServerlessCustomDomain { }, }; this.hooks = { - "after:deploy:deploy": this.hookWrapper.bind(this, this.setupBasePathMapping), - "after:info:info": this.hookWrapper.bind(this, this.domainSummary), + "after:deploy:deploy": this.hookWrapper.bind(this, this.setupBasePathMappings), + "after:info:info": this.hookWrapper.bind(this, this.domainSummaries), "before:deploy:deploy": this.hookWrapper.bind(this, this.updateCloudFormationOutputs), - "before:remove:remove": this.hookWrapper.bind(this, this.removeBasePathMapping), - "create_domain:create": this.hookWrapper.bind(this, this.createDomain), - "delete_domain:delete": this.hookWrapper.bind(this, this.deleteDomain), + "before:remove:remove": this.hookWrapper.bind(this, this.removeBasePathMappings), + "create_domain:create": this.hookWrapper.bind(this, this.createDomains), + "delete_domain:delete": this.hookWrapper.bind(this, this.deleteDomains), }; } @@ -75,199 +65,255 @@ class ServerlessCustomDomain { * @param lifecycleFunc lifecycle function that actually does desired action */ public async hookWrapper(lifecycleFunc: any) { + this.initializeVariables(); - if (!this.enabled) { - this.serverless.cli.log("serverless-domain-manager: Custom domain is disabled."); - return; - } else { - return await lifecycleFunc.call(this); - } + + return await lifecycleFunc.call(this); } /** * Lifecycle function to create a domain * Wraps creating a domain and resource record set */ - public async createDomain(): Promise { - let domainInfo; - try { - domainInfo = await this.getDomainInfo(); - } catch (err) { - if (err.message !== `Error: ${this.givenDomainName} not found.`) { - throw err; + public async createDomains(): Promise { + + await this.getDomainInfo(); + + await Promise.all(this.domains.map(async (domain) => { + try { + if (!domain.domainInfo) { + + domain.certificateArn = await this.getCertArn(domain); + + await this.createCustomDomain(domain); + + await this.changeResourceRecordSet("UPSERT", domain); + + this.serverless.cli.log( + `Custom domain ${domain.givenDomainName} was created. + New domains may take up to 40 minutes to be initialized.`, + ); + } else { + this.serverless.cli.log(`Custom domain ${domain.givenDomainName} already exists.`); + } + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Unable to craete domain ${domain.givenDomainName}`); } - } - if (!domainInfo) { - const certArn = await this.getCertArn(); - domainInfo = await this.createCustomDomain(certArn); - await this.changeResourceRecordSet("UPSERT", domainInfo); - this.serverless.cli.log( - `Custom domain ${this.givenDomainName} was created. - New domains may take up to 40 minutes to be initialized.`, - ); - } else { - this.serverless.cli.log(`Custom domain ${this.givenDomainName} already exists.`); - } + })); } /** * Lifecycle function to delete a domain * Wraps deleting a domain and resource record set */ - public async deleteDomain(): Promise { - let domainInfo; - try { - domainInfo = await this.getDomainInfo(); - } catch (err) { - if (err.message === `Error: ${this.givenDomainName} not found.`) { - this.serverless.cli.log(`Unable to delete custom domain ${this.givenDomainName}.`); - return; + public async deleteDomains(): Promise { + + await this.getDomainInfo(); + + await Promise.all(this.domains.map(async (domain) => { + try { + if (domain.domainInfo) { + await this.deleteCustomDomain(domain); + await this.changeResourceRecordSet("DELETE", domain); + domain.domainInfo = undefined; + this.serverless.cli.log(`Custom domain ${domain.givenDomainName} was deleted.`); + } else { + this.serverless.cli.log(`Custom domain ${domain.givenDomainName} does not exists.`); + } + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Unable to delete domain ${domain.givenDomainName}`); } - throw err; - } - await this.deleteCustomDomain(); - await this.changeResourceRecordSet("DELETE", domainInfo); - this.serverless.cli.log(`Custom domain ${this.givenDomainName} was deleted.`); + })); } /** * Lifecycle function to add domain info to the CloudFormation stack's Outputs */ public async updateCloudFormationOutputs(): Promise { - const domainInfo = await this.getDomainInfo(); - this.addOutputs(domainInfo); + + await this.getDomainInfo(); + + await Promise.all(this.domains.map(async (domain) => { + this.addOutputs(domain); + })); } /** * Lifecycle function to create basepath mapping * Wraps creation of basepath mapping and adds domain name info as output to cloudformation stack */ - public async setupBasePathMapping(): Promise { - // check if basepathmapping exists - const restApiId = await this.getRestApiId(); - const currentBasePath = await this.getBasePathMapping(restApiId); - // if basepath that matches restApiId exists, update; else, create - if (!currentBasePath) { - await this.createBasePathMapping(restApiId); - } else { - await this.updateBasePathMapping(currentBasePath); - } - const domainInfo = await this.getDomainInfo(); - await this.printDomainSummary(domainInfo); + public async setupBasePathMappings(): Promise { + await Promise.all(this.domains.map(async (domain) => { + try { + domain.apiId = await this.getApiId(domain); + + domain.apiMapping = await this.getBasePathMapping(domain); + + if (!domain.apiMapping) { + await this.createBasePathMapping(domain); + } else { + await this.updateBasePathMapping(domain); + } + + await this.getDomainInfo(); + // this.addOutputs(domain); + + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Unable to setup base domain mappings for ${domain.givenDomainName}`); + } + })).then(() => { + // Print summary upon completion + this.domains.forEach((domain) => { + this.printDomainSummary(domain); + }); + }); } /** * Lifecycle function to delete basepath mapping * Wraps deletion of basepath mapping */ - public async removeBasePathMapping(): Promise { - await this.deleteBasePathMapping(); + public async removeBasePathMappings(): Promise { + await Promise.all(this.domains.map(async (domain) => { + try { + domain.apiId = await this.getApiId(domain); + + // Unable to find the correspond API, manuall clean up will be required + if (!domain.apiId) { + this.serverless.cli.log(`Unable to find corresponding API for ${domain.givenDomainName}, + API Mappings may need to be manually removed.`, "Serverless Domain Manager"); + } else { + domain.apiMapping = await this.getBasePathMapping(domain); + await this.deleteBasePathMapping(domain); + } + } catch (err) { + if (err.message.indexOf("Failed to find CloudFormation") > -1) { + this.serverless.cli.log(`Unable to find Cloudformation Stack for ${domain.givenDomainName}, + API Mappings may need to be manually removed.`, "Serverless Domain Manager"); + } else { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Unable to remove base bath mappings for domain ${domain.givenDomainName}`); + } + } + })); } /** * Lifecycle function to print domain summary * Wraps printing of all domain manager related info */ - public async domainSummary(): Promise { - const domainInfo = await this.getDomainInfo(); - if (domainInfo) { - this.printDomainSummary(domainInfo); - } else { - this.serverless.cli.log("Unable to print Serverless Domain Manager Summary"); - } + public async domainSummaries(): Promise { + await this.getDomainInfo(); + + this.domains.forEach((domain) => { + if (domain.domainInfo) { + this.printDomainSummary(domain); + } else { + this.serverless.cli.log( + `Unable to print Serverless Domain Manager Summary for ${domain.givenDomainName}`, + ); + } + }); } /** * Goes through custom domain property and initializes local variables and cloudformation template */ public initializeVariables(): void { - this.enabled = this.evaluateEnabled(); - if (this.enabled) { - const credentials = this.serverless.providers.aws.getCredentials(); - credentials.region = this.serverless.providers.aws.getRegion(); - - this.serverless.providers.aws.sdk.config.update({maxRetries: 20}); - this.apigateway = new this.serverless.providers.aws.sdk.APIGateway(credentials); - this.route53 = new this.serverless.providers.aws.sdk.Route53(credentials); - this.cloudformation = new this.serverless.providers.aws.sdk.CloudFormation(credentials); - - this.givenDomainName = this.serverless.service.custom.customDomain.domainName; - this.hostedZonePrivate = this.serverless.service.custom.customDomain.hostedZonePrivate; - let basePath = this.serverless.service.custom.customDomain.basePath; - if (basePath == null || basePath.trim() === "") { - basePath = "(none)"; - } - this.basePath = basePath; - let stage = this.serverless.service.custom.customDomain.stage; - if (typeof stage === "undefined") { - stage = this.options.stage || this.serverless.service.provider.stage; - } - this.stage = stage; - const endpointTypeWithDefault = this.serverless.service.custom.customDomain.endpointType || - endpointTypes.edge; - const endpointTypeToUse = endpointTypes[endpointTypeWithDefault.toLowerCase()]; - if (!endpointTypeToUse) { - throw new Error(`${endpointTypeWithDefault} is not supported endpointType, use edge or regional.`); - } - this.endpointType = endpointTypeToUse; + // Make sure customDomain configuration exists, stop if not + if (typeof this.serverless.service.custom === "undefined" + || typeof this.serverless.service.custom.customDomain === "undefined") { + throw new Error("serverless-domain-manager: Plugin configuration is missing."); + } + + const credentials = this.serverless.providers.aws.getCredentials(); + credentials.region = this.serverless.providers.aws.getRegion(); - const securityPolicyDefault = this.serverless.service.custom.customDomain.securityPolicy || - tlsVersions.tls_1_2; - const tlsVersionToUse = tlsVersions[securityPolicyDefault.toLowerCase()]; - if (!tlsVersionToUse) { - throw new Error(`${securityPolicyDefault} is not a supported securityPolicy, use tls_1_0 or tls_1_2.`); + this.serverless.providers.aws.sdk.config.update({ maxRetries: 20 }); + this.apigateway = new this.serverless.providers.aws.sdk.APIGateway(credentials); + this.apigatewayV2 = new this.serverless.providers.aws.sdk.ApiGatewayV2(credentials); + this.route53 = new this.serverless.providers.aws.sdk.Route53(credentials); + this.cloudformation = new this.serverless.providers.aws.sdk.CloudFormation(credentials); + + // Loop over the domain configurations and popluates the domains array with DomainConfigs + this.domains = []; + + // If the key of the item in config is an api type it is using per api type domain structure + if (Globals.apiTypes[Object.keys(this.serverless.service.custom.customDomain)[0]]) { + for (const configApiType in this.serverless.service.custom.customDomain) { + if (Globals.apiTypes[configApiType]) { // If statement check to follow tslint + this.serverless.service.custom.customDomain[configApiType].apiType = configApiType; + this.domains.push(new DomainConfig(this.serverless.service.custom.customDomain[configApiType])); + } else { + throw Error(`Error: Invalud API Type, ${configApiType}`); + } } - this.securityPolicy = tlsVersionToUse; + } else { // Default to single domain config + this.domains.push(new DomainConfig(this.serverless.service.custom.customDomain)); + } - this.acmRegion = this.endpointType === endpointTypes.regional ? + // Filter inactive domains + this.domains = this.domains.filter((domain) => domain.enabled); + + // Set ACM Region on the domain configs + for (const dc of this.domains) { + this.acmRegion = dc.endpointType === Globals.endpointTypes.regional ? this.serverless.providers.aws.getRegion() : "us-east-1"; const acmCredentials = Object.assign({}, credentials, { region: this.acmRegion }); this.acm = new this.serverless.providers.aws.sdk.ACM(acmCredentials); } + + // Validate the domain configuraitons + this.validateDomainConfigs(); } /** - * Determines whether this plug-in is enabled. - * - * This method reads the customDomain property "enabled" to see if this plug-in should be enabled. - * If the property's value is undefined, a default value of true is assumed (for backwards - * compatibility). - * If the property's value is provided, this should be boolean, otherwise an exception is thrown. - * If no customDomain object exists, an exception is thrown. + * Validates domain configs to make sure they are valid, ie HTTP api cannot be used with EDGE domain */ - public evaluateEnabled(): boolean { - if (typeof this.serverless.service.custom === "undefined" - || typeof this.serverless.service.custom.customDomain === "undefined") { - throw new Error("serverless-domain-manager: Plugin configuration is missing."); - } + public validateDomainConfigs() { + this.domains.forEach((domain) => { - const enabled = this.serverless.service.custom.customDomain.enabled; - if (enabled === undefined) { - return true; - } - if (typeof enabled === "boolean") { - return enabled; - } else if (typeof enabled === "string" && enabled === "true") { - return true; - } else if (typeof enabled === "string" && enabled === "false") { - return false; - } - throw new Error(`serverless-domain-manager: Ambiguous enablement boolean: "${enabled}"`); + // Show warning if allowPathMatching is set to true + if (domain.allowPathMatching) { + this.serverless.cli.log(`WARNING: "allowPathMatching" is set for ${domain.givenDomainName}. + This should only be used when migrating a path to a different API type. e.g. REST to HTTP.`); + } + + if (domain.apiType === Globals.apiTypes.rest) { + // Currently no validation for REST API types + + } else if (domain.apiType === Globals.apiTypes.http) { // Validation for http apis + // HTTP Apis do not support edge domains + if (domain.endpointType === Globals.endpointTypes.edge) { + throw Error(`Error: 'edge' endpointType is not compatible with HTTP APIs`); + } + + } else if (domain.apiType === Globals.apiTypes.websocket) { // Validation for WebSocket apis + // Websocket Apis do not support edge domains + if (domain.endpointType === Globals.endpointTypes.edge) { + throw Error(`Error: 'edge' endpointType is not compatible with WebSocket APIs`); + } + } + }); } /** * Gets Certificate ARN that most closely matches domain name OR given Cert ARN if provided */ - public async getCertArn(): Promise { - if (this.serverless.service.custom.customDomain.certificateArn) { - this.serverless.cli.log( - `Selected specific certificateArn ${this.serverless.service.custom.customDomain.certificateArn}`); - return this.serverless.service.custom.customDomain.certificateArn; + public async getCertArn(domain: DomainConfig): Promise { + if (domain.certificateArn) { + this.serverless.cli.log(`Selected specific certificateArn ${domain.certificateArn}`); + return domain.certificateArn; } let certificateArn; // The arn of the choosen certificate - let certificateName = this.serverless.service.custom.customDomain.certificateName; // The certificate name + + let certificateName = domain.certificateName; // The certificate name + try { let certificates = []; let nextToken; @@ -289,7 +335,7 @@ class ServerlessCustomDomain { certificateArn = foundCertificate.CertificateArn; } } else { - certificateName = this.givenDomainName; + certificateName = domain.givenDomainName; certificates.forEach((certificate) => { let certificateListName = certificate.DomainName; // Looks for wild card and takes it out when checking @@ -306,7 +352,7 @@ class ServerlessCustomDomain { }); } } catch (err) { - this.logIfDebug(err); + this.logIfDebug(err, domain.givenDomainName); throw Error(`Error: Could not list certificates in Certificate Manager.\n${err}`); } if (certificateArn == null) { @@ -316,68 +362,87 @@ class ServerlessCustomDomain { } /** - * Gets domain info as DomainInfo object if domain exists, otherwise returns false + * Populates the DomainInfo object on the Domains if custom domain in aws exists */ - public async getDomainInfo(): Promise { - let domainInfo; - try { - domainInfo = await this.apigateway.getDomainName({ domainName: this.givenDomainName }).promise(); - return new DomainInfo(domainInfo); - } catch (err) { - this.logIfDebug(err); - if (err.code === "NotFoundException") { - throw new Error(`Error: ${this.givenDomainName} not found.`); + public async getDomainInfo(): Promise { + await Promise.all(this.domains.map(async (domain) => { + try { + const domainInfo = await this.apigatewayV2.getDomainName({ + DomainName: domain.givenDomainName, + }).promise(); + + domain.domainInfo = new DomainInfo(domainInfo); + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + if (err.code !== "NotFoundException") { + throw new Error(`Error: Unable to fetch information about ${domain.givenDomainName}`); + } } - throw new Error(`Error: Unable to fetch information about ${this.givenDomainName}`); - } + })); } /** * Creates Custom Domain Name through API Gateway * @param certificateArn: Certificate ARN to use for custom domain */ - public async createCustomDomain(certificateArn: string): Promise { - // Set up parameters - const params = { - certificateArn, - domainName: this.givenDomainName, - endpointConfiguration: { - types: [this.endpointType], - }, - regionalCertificateArn: certificateArn, - securityPolicy: this.securityPolicy, - }; - if (this.endpointType === endpointTypes.edge) { - params.regionalCertificateArn = undefined; - } else if (this.endpointType === endpointTypes.regional) { - params.certificateArn = undefined; - } + public async createCustomDomain(domain: DomainConfig): Promise { - // Make API call let createdDomain = {}; - try { - createdDomain = await this.apigateway.createDomainName(params).promise(); - } catch (err) { - this.logIfDebug(err); - throw new Error(`Error: Failed to create custom domain ${this.givenDomainName}\n`); + + // For EDGE domain name, create with APIGateway (v1) + if (domain.endpointType === Globals.endpointTypes.edge) { + // Set up parameters + const params = { + certificateArn: domain.certificateArn, + domainName: domain.givenDomainName, + endpointConfiguration: { + types: [domain.endpointType], + }, + securityPolicy: domain.securityPolicy, + }; + + // Make API call to create domain + try { + // If creating REST api use v1 of api gateway, else use v2 for HTTP and Websocket + createdDomain = await this.apigateway.createDomainName(params).promise(); + domain.domainInfo = new DomainInfo(createdDomain); + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Failed to create custom domain ${domain.givenDomainName}\n`); + } + + } else { // For Regional domain name create with ApiGatewayV2 + const params = { + DomainName: domain.givenDomainName, + DomainNameConfigurations: [{ + CertificateArn: domain.certificateArn, + EndpointType: domain.endpointType, + SecurityPolicy: domain.securityPolicy, + }], + }; + + // Make API call to create domain + try { + // If creating REST api use v1 of api gateway, else use v2 for HTTP and Websocket + createdDomain = await this.apigatewayV2.createDomainName(params).promise(); + domain.domainInfo = new DomainInfo(createdDomain); + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Failed to create custom domain ${domain.givenDomainName}\n`); + } } - return new DomainInfo(createdDomain); } /** * Delete Custom Domain Name through API Gateway */ - public async deleteCustomDomain(): Promise { - const params = { - domainName: this.givenDomainName, - }; - + public async deleteCustomDomain(domain: DomainConfig): Promise { // Make API call try { - await this.apigateway.deleteDomainName(params).promise(); + await this.apigatewayV2.deleteDomainName({DomainName: domain.givenDomainName}).promise(); } catch (err) { - this.logIfDebug(err); - throw new Error(`Error: Failed to delete custom domain ${this.givenDomainName}\n`); + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Failed to delete custom domain ${domain.givenDomainName}\n`); } } @@ -386,28 +451,28 @@ class ServerlessCustomDomain { * @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: DomainInfo): Promise { + public async changeResourceRecordSet(action: string, domain: DomainConfig): Promise { if (action !== "UPSERT" && action !== "DELETE") { throw new Error(`Error: Invalid action "${action}" when changing Route53 Record. Action must be either UPSERT or DELETE.\n`); } - const createRoute53Record = this.serverless.service.custom.customDomain.createRoute53Record; + const createRoute53Record = domain.createRoute53Record; if (createRoute53Record !== undefined && createRoute53Record === false) { - this.serverless.cli.log("Skipping creation of Route53 record."); + this.serverless.cli.log(`Skipping ${action === "DELETE" ? "removal" : "creation"} of Route53 record.`); return; } // Set up parameters - const route53HostedZoneId = await this.getRoute53HostedZoneId(); + const route53HostedZoneId = await this.getRoute53HostedZoneId(domain); const Changes = ["A", "AAAA"].map((Type) => ({ Action: action, ResourceRecordSet: { AliasTarget: { - DNSName: domain.domainName, + DNSName: domain.domainInfo.domainName, EvaluateTargetHealth: false, - HostedZoneId: domain.hostedZoneId, + HostedZoneId: domain.domainInfo.hostedZoneId, }, - Name: this.givenDomainName, + Name: domain.givenDomainName, Type, }, })); @@ -422,30 +487,30 @@ class ServerlessCustomDomain { try { await this.route53.changeResourceRecordSets(params).promise(); } catch (err) { - this.logIfDebug(err); - throw new Error(`Error: Failed to ${action} A Alias for ${this.givenDomainName}\n`); + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Failed to ${action} A Alias for ${domain.givenDomainName}\n`); } } /** * Gets Route53 HostedZoneId from user or from AWS */ - public async getRoute53HostedZoneId(): Promise { - if (this.serverless.service.custom.customDomain.hostedZoneId) { + public async getRoute53HostedZoneId(domain: DomainConfig): Promise { + if (domain.hostedZoneId) { this.serverless.cli.log( `Selected specific hostedZoneId ${this.serverless.service.custom.customDomain.hostedZoneId}`); - return this.serverless.service.custom.customDomain.hostedZoneId; + return domain.hostedZoneId; } - const filterZone = this.hostedZonePrivate !== undefined; - if (filterZone && this.hostedZonePrivate) { + const filterZone = domain.hostedZonePrivate !== undefined; + if (filterZone && domain.hostedZonePrivate) { this.serverless.cli.log("Filtering to only private zones."); - } else if (filterZone && !this.hostedZonePrivate) { + } else if (filterZone && !domain.hostedZonePrivate) { this.serverless.cli.log("Filtering to only public zones."); } let hostedZoneData; - const givenDomainNameReverse = this.givenDomainName.split(".").reverse(); + const givenDomainNameReverse = domain.givenDomainName.split(".").reverse(); try { hostedZoneData = await this.route53.listHostedZones({}).promise(); @@ -457,7 +522,7 @@ class ServerlessCustomDomain { } else { hostedZoneName = hostedZone.Name; } - if (!filterZone || this.hostedZonePrivate === hostedZone.Config.PrivateZone) { + if (!filterZone || domain.hostedZonePrivate === hostedZone.Config.PrivateZone) { const hostedZoneNameReverse = hostedZoneName.split(".").reverse(); if (givenDomainNameReverse.length === 1 @@ -483,93 +548,145 @@ class ServerlessCustomDomain { return hostedZoneId.substring(startPos, endPos); } } catch (err) { - this.logIfDebug(err); + this.logIfDebug(err, domain.givenDomainName); throw new Error(`Error: Unable to list hosted zones in Route53.\n${err}`); } - throw new Error(`Error: Could not find hosted zone "${this.givenDomainName}"`); + throw new Error(`Error: Could not find hosted zone "${domain.givenDomainName}"`); } - public async getBasePathMapping(restApiId: string): Promise { + public async getBasePathMapping(domain: DomainConfig): Promise { const params = { - domainName: this.givenDomainName, + DomainName: domain.givenDomainName, }; - let basepathInfo; - let currentBasePath; try { - basepathInfo = await this.apigateway.getBasePathMappings(params).promise(); - } catch (err) { - this.logIfDebug(err); - throw new Error(`Error: Unable to get BasePathMappings for ${this.givenDomainName}`); - } - if (basepathInfo.items !== undefined && basepathInfo.items instanceof Array) { - for (const basepathObj of basepathInfo.items) { - if (basepathObj.restApiId === restApiId) { - currentBasePath = basepathObj.basePath; - break; + const mappings = await this.apigatewayV2.getApiMappings(params).promise(); + + if (mappings.Items.length === 0) { + return; + } else { + for (const mapping of mappings.Items) { + if (mapping.ApiId === domain.apiId + || (mapping.ApiMappingKey === domain.basePath && domain.allowPathMatching) ) { + return mapping; + } } } + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Unable to get API Mappings for ${domain.givenDomainName}`); } - return currentBasePath; } /** * Creates basepath mapping */ - public async createBasePathMapping(restApiId: string): Promise { - const params = { - basePath: this.basePath, - domainName: this.givenDomainName, - restApiId, - stage: this.stage, - }; - // Make API call - try { - await this.apigateway.createBasePathMapping(params).promise(); - this.serverless.cli.log("Created basepath mapping."); - } catch (err) { - this.logIfDebug(err); - throw new Error(`Error: Unable to create basepath mapping.\n`); + public async createBasePathMapping(domain: DomainConfig): Promise { + // Use APIGateway (v1) for EDGE domains + if (domain.endpointType === Globals.endpointTypes.edge) { + const params = { + basePath: domain.basePath, + domainName: domain.givenDomainName, + restApiId: domain.apiId, + stage: domain.stage, + }; + // Make API call + try { + await this.apigateway.createBasePathMapping(params).promise(); + this.serverless.cli.log(`Created API mapping '${domain.basePath}' for ${domain.givenDomainName}`); + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: ${domain.givenDomainName}: Unable to create basepath mapping.\n`); + } + + } else { // Use ApiGatewayV2 for Regional domains + const params = { + ApiId: domain.apiId, + ApiMappingKey: domain.basePath, + DomainName: domain.givenDomainName, + Stage: domain.apiType === Globals.apiTypes.http ? "$default" : domain.stage, + }; + // Make API call + try { + await this.apigatewayV2.createApiMapping(params).promise(); + this.serverless.cli.log(`Created API mapping '${domain.basePath}' for ${domain.givenDomainName}`); + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: ${domain.givenDomainName}: Unable to create basepath mapping.\n`); + } } } /** * Updates basepath mapping */ - public async updateBasePathMapping(oldBasePath: string): Promise { - const params = { - basePath: oldBasePath, - domainName: this.givenDomainName, - patchOperations: [ - { - op: "replace", - path: "/basePath", - value: this.basePath, - }, - ], - }; - // Make API call - try { - await this.apigateway.updateBasePathMapping(params).promise(); - this.serverless.cli.log("Updated basepath mapping."); - } catch (err) { - this.logIfDebug(err); - throw new Error(`Error: Unable to update basepath mapping.\n`); + public async updateBasePathMapping(domain: DomainConfig): Promise { + // Use APIGateway (v1) for EDGE domains + if (domain.endpointType === Globals.endpointTypes.edge) { + const params = { + basePath: domain.apiMapping.ApiMappingKey, + domainName: domain.givenDomainName, + patchOperations: [ + { + op: "replace", + path: "/basePath", + value: domain.basePath, + }, + ], + }; + + // Make API call + try { + await this.apigateway.updateBasePathMapping(params).promise(); + this.serverless.cli.log(`Updated API mapping from '${domain.apiMapping.ApiMappingKey}' + to '${domain.basePath}' for ${domain.givenDomainName}`); + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: ${domain.givenDomainName}: Unable to update basepath mapping.\n`); + } + + } else { // Use ApiGatewayV2 for Regional domains + + const params = { + ApiId: domain.apiId, + ApiMappingId: domain.apiMapping.ApiMappingId, + ApiMappingKey: domain.basePath, + DomainName: domain.givenDomainName, + Stage: domain.apiType === Globals.apiTypes.http ? "$default" : domain.stage, + }; + + // Make API call + try { + await this.apigatewayV2.updateApiMapping(params).promise(); + this.serverless.cli.log(`Updated API mapping to '${domain.basePath}' for ${domain.givenDomainName}`); + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: ${domain.givenDomainName}: Unable to update basepath mapping.\n`); + } } } /** * Gets rest API id from CloudFormation stack */ - public async getRestApiId(): Promise { + public async getApiId(domain: DomainConfig): Promise { if (this.serverless.service.provider.apiGateway && this.serverless.service.provider.apiGateway.restApiId) { this.serverless.cli.log(`Mapping custom domain to existing API ${this.serverless.service.provider.apiGateway.restApiId}.`); return this.serverless.service.provider.apiGateway.restApiId; } + const stackName = this.serverless.service.provider.stackName || - `${this.serverless.service.service}-${this.stage}`; + `${this.serverless.service.service}-${domain.stage}`; + + let LogicalResourceId = "ApiGatewayRestApi"; + if (domain.apiType === Globals.apiTypes.http) { + LogicalResourceId = "HttpApi"; + } else if (domain.apiType === Globals.apiTypes.websocket) { + LogicalResourceId = "WebsocketsApi"; + } + const params = { - LogicalResourceId: "ApiGatewayRestApi", + LogicalResourceId, StackName: stackName, }; @@ -577,51 +694,72 @@ class ServerlessCustomDomain { try { response = await this.cloudformation.describeStackResource(params).promise(); } catch (err) { - this.logIfDebug(err); - throw new Error(`Error: Failed to find CloudFormation resources for ${this.givenDomainName}\n`); + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Failed to find CloudFormation resources for ${domain.givenDomainName}\n`); } - const restApiId = response.StackResourceDetail.PhysicalResourceId; - if (!restApiId) { - throw new Error(`Error: No RestApiId associated with CloudFormation stack ${stackName}`); + + const apiId = response.StackResourceDetail.PhysicalResourceId; + if (!apiId) { + throw new Error(`Error: No ApiId associated with CloudFormation stack ${stackName}`); } - return restApiId; + return apiId; } /** * Deletes basepath mapping */ - public async deleteBasePathMapping(): Promise { + public async deleteBasePathMapping(domain: DomainConfig): Promise { const params = { - basePath: this.basePath, - domainName: this.givenDomainName, + ApiMappingId: domain.apiMapping.ApiMappingId, + DomainName: domain.givenDomainName, }; + // Make API call try { - await this.apigateway.deleteBasePathMapping(params).promise(); + await this.apigatewayV2.deleteApiMapping(params).promise(); this.serverless.cli.log("Removed basepath mapping."); } catch (err) { - this.logIfDebug(err); - this.serverless.cli.log("Unable to remove basepath mapping."); + this.logIfDebug(err, domain.givenDomainName); + this.serverless.cli.log(`Unable to remove basepath mapping for ${domain.givenDomainName}`); } } /** * Adds the domain name and distribution domain name to the CloudFormation outputs */ - public addOutputs(domainInfo: DomainInfo): void { + public addOutputs(domain: DomainConfig): void { const service = this.serverless.service; if (!service.provider.compiledCloudFormationTemplate.Outputs) { service.provider.compiledCloudFormationTemplate.Outputs = {}; } - service.provider.compiledCloudFormationTemplate.Outputs.DistributionDomainName = { - Value: domainInfo.domainName, + + // Defaults for REST and backwards compatibility + let distributionDomainNameOutputKey = "DistributionDomainName"; + let domainNameOutputKey = "DomainName"; + let hostedZoneIdOutputKey = "HostedZoneId"; + + if (domain.apiType === Globals.apiTypes.http) { + distributionDomainNameOutputKey += "Http"; + domainNameOutputKey += "Http"; + hostedZoneIdOutputKey += "Http"; + + } else if (domain.apiType === Globals.apiTypes.websocket) { + distributionDomainNameOutputKey += "Websocket"; + domainNameOutputKey += "Websocket"; + hostedZoneIdOutputKey += "Websocket"; + } + + service.provider.compiledCloudFormationTemplate.Outputs[distributionDomainNameOutputKey] = { + Value: domain.domainInfo.domainName, }; - service.provider.compiledCloudFormationTemplate.Outputs.DomainName = { - Value: this.givenDomainName, + + service.provider.compiledCloudFormationTemplate.Outputs[domainNameOutputKey] = { + Value: domain.givenDomainName, }; - if (domainInfo.hostedZoneId) { - service.provider.compiledCloudFormationTemplate.Outputs.HostedZoneId = { - Value: domainInfo.hostedZoneId, + + if (domain.domainInfo.hostedZoneId) { + service.provider.compiledCloudFormationTemplate.Outputs[hostedZoneIdOutputKey] = { + Value: domain.domainInfo.hostedZoneId, }; } } @@ -630,26 +768,23 @@ class ServerlessCustomDomain { * Logs message if SLS_DEBUG is set * @param message message to be printed */ - public logIfDebug(message: any): void { + public logIfDebug(message: any, domain?: string): void { if (process.env.SLS_DEBUG) { - this.serverless.cli.log(message, "Serverless Domain Manager"); + this.serverless.cli.log(`Error: ${domain ? domain + ": " : ""} ${message}`, "Serverless Domain Manager"); } } /** * Prints out a summary of all domain manager related info */ - private printDomainSummary(domainInfo: DomainInfo): void { - this.serverless.cli.consoleLog(chalk.yellow.underline("\nServerless Domain Manager Summary")); - if (this.serverless.service.custom.customDomain.createRoute53Record !== false) { - this.serverless.cli.consoleLog(chalk.yellow("Domain Name")); - this.serverless.cli.consoleLog(` ${this.givenDomainName}`); - } + private printDomainSummary(domain: DomainConfig): void { + this.serverless.cli.consoleLog(chalk.yellow.underline("\nServerless Domain Manager Summary")); this.serverless.cli.consoleLog(chalk.yellow("Distribution Domain Name")); - this.serverless.cli.consoleLog(` Target Domain: ${domainInfo.domainName}`); - this.serverless.cli.consoleLog(` Hosted Zone Id: ${domainInfo.hostedZoneId}`); + this.serverless.cli.consoleLog(` Domain Name: ${domain.givenDomainName}`); + this.serverless.cli.consoleLog(` Target Domain: ${domain.domainInfo.domainName}`); + this.serverless.cli.consoleLog(` Hosted Zone Id: ${domain.domainInfo.hostedZoneId}`); } } diff --git a/package-lock.json b/package-lock.json index c918042e..9b598098 100644 --- a/package-lock.json +++ b/package-lock.json @@ -671,23 +671,28 @@ } }, "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "dev": true, "requires": { - "es-to-primitive": "^1.2.0", + "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" } }, "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "requires": { "is-callable": "^1.1.4", @@ -1032,9 +1037,9 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true }, "hasha": { @@ -1121,15 +1126,15 @@ "dev": true }, "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", "dev": true }, "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", "dev": true }, "is-fullwidth-code-point": { @@ -1145,12 +1150,12 @@ "dev": true }, "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", "dev": true, "requires": { - "has": "^1.0.1" + "has": "^1.0.3" } }, "is-stream": { @@ -1160,12 +1165,12 @@ "dev": true }, "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "dev": true, "requires": { - "has-symbols": "^1.0.0" + "has-symbols": "^1.0.1" } }, "is-typedarray": { @@ -1766,20 +1771,38 @@ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "dev": true }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, "object.entries": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", - "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz", + "integrity": "sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", + "es-abstract": "^1.17.0-next.1", "function-bind": "^1.1.1", "has": "^1.0.3" } @@ -2370,6 +2393,26 @@ } } }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts index f31dc23b..cf310f96 100644 --- a/test/unit-tests/index.test.ts +++ b/test/unit-tests/index.test.ts @@ -4,6 +4,8 @@ import chai = require("chai"); import spies = require("chai-spies"); import "mocha"; import DomainInfo = require("../../DomainInfo"); +import DomainConfig = require("../../DomainConfig"); +import Globals from "../../Globals"; import ServerlessCustomDomain = require("../../index"); import { ServerlessInstance, ServerlessOptions } from "../../types"; @@ -49,6 +51,7 @@ const constructPlugin = (customDomainOptions) => { sdk: { ACM: aws.ACM, APIGateway: aws.APIGateway, + ApiGatewayV2: aws.ApiGatewayV2, CloudFormation: aws.CloudFormation, Route53: aws.Route53, config: { @@ -60,6 +63,7 @@ const constructPlugin = (customDomainOptions) => { service: { custom: { customDomain: { + apiType: customDomainOptions.apiType, basePath: customDomainOptions.basePath, certificateArn: customDomainOptions.certificateArn, certificateName: customDomainOptions.certificateName, @@ -116,24 +120,68 @@ describe("Custom Domain Plugin", () => { } expect(errored).to.equal(true); }); + + it("Unsupported api type throw exception", () => { + const plugin = constructPlugin({ apiType: "notSupported" }); + + let errored = false; + try { + plugin.initializeVariables(); + } catch (err) { + errored = true; + expect(err.message).to.equal("notSupported is not supported api type, use REST, HTTP or WEBSOCKET."); + } + expect(errored).to.equal(true); + }); + + it("Unsupported HTTP EDGE endpoint configuration", () => { + const plugin = constructPlugin({ apiType: "http" }); + + let errored = false; + try { + plugin.initializeVariables(); + } catch (err) { + errored = true; + expect(err.message).to.equal("Error: 'edge' endpointType is not compatible with HTTP APIs"); + } + expect(errored).to.equal(true); + }); + + it("Unsupported WS EDGE endpoint configuration", () => { + const plugin = constructPlugin({ apiType: "websocket" }); + + let errored = false; + try { + plugin.initializeVariables(); + } catch (err) { + errored = true; + expect(err.message).to.equal("Error: 'edge' endpointType is not compatible with WebSocket APIs"); + } + expect(errored).to.equal(true); + }); + }); describe("Set Domain Name and Base Path", () => { - it("Creates basepath mapping", async () => { + it("Creates basepath mapping for edge REST api", async () => { AWS.mock("APIGateway", "createBasePathMapping", (params, callback) => { callback(null, params); }); const plugin = constructPlugin({ basePath: "test_basepath", domainName: "test_domain", + endpointType: "edge", }); plugin.initializeVariables(); plugin.apigateway = new aws.APIGateway(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; - plugin.basePath = plugin.serverless.service.custom.customDomain.basePath; + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + dc.apiId = "test_rest_api_id"; + const spy = chai.spy.on(plugin.apigateway, "createBasePathMapping"); - await plugin.createBasePathMapping("test_rest_api_id"); + await plugin.createBasePathMapping(dc); + expect(spy).to.have.been.called.with({ basePath: "test_basepath", domainName: "test_domain", @@ -142,7 +190,35 @@ describe("Custom Domain Plugin", () => { }); }); - it("Updates basepath mapping", async () => { + it("Creates basepath mapping for regional HTTP/Websocket api", async () => { + AWS.mock("ApiGatewayV2", "createApiMapping", (params, callback) => { + callback(null, params); + }); + const plugin = constructPlugin({ + apiType: "http", + basePath: "test_basepath", + domainName: "test_domain", + endpointType: "regional", + }); + plugin.initializeVariables(); + plugin.apigatewayV2 = new aws.ApiGatewayV2(); + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + dc.apiId = "test_rest_api_id"; + + const spy = chai.spy.on(plugin.apigatewayV2, "createApiMapping"); + + await plugin.createBasePathMapping(dc); + expect(spy).to.have.been.called.with({ + ApiId: "test_rest_api_id", + ApiMappingKey: "test_basepath", + DomainName: "test_domain", + Stage: "$default", + }); + }); + + it("Updates basepath mapping for a edge REST api", async () => { AWS.mock("APIGateway", "updateBasePathMapping", (params, callback) => { callback(null, params); }); @@ -151,12 +227,16 @@ describe("Custom Domain Plugin", () => { domainName: "test_domain", }); plugin.initializeVariables(); + plugin.apigateway = new aws.APIGateway(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; - plugin.basePath = plugin.serverless.service.custom.customDomain.basePath; + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + dc.apiMapping = {ApiMappingKey: "old_basepath"}; + const spy = chai.spy.on(plugin.apigateway, "updateBasePathMapping"); - await plugin.updateBasePathMapping("old_basepath"); + await plugin.updateBasePathMapping(dc); expect(spy).to.have.been.called.with({ basePath: "old_basepath", domainName: "test_domain", @@ -170,15 +250,93 @@ describe("Custom Domain Plugin", () => { }); }); + it("Updates basepath mapping for regional HTTP/WS api", async () => { + AWS.mock("ApiGatewayV2", "updateApiMapping", (params, callback) => { + callback(null, params); + }); + const plugin = constructPlugin({ + apiType: "http", + basePath: "test_basepath", + domainName: "test_domain", + endpointType: "regional", + }); + plugin.initializeVariables(); + + plugin.apigatewayV2 = new aws.ApiGatewayV2(); + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + dc.apiId = "test_api_id", + dc.apiMapping = {ApiMappingId: "test_mapping_id"}; + + const spy = chai.spy.on(plugin.apigatewayV2, "updateApiMapping"); + + await plugin.updateBasePathMapping(dc); + expect(spy).to.have.been.called.with({ + ApiId: "test_api_id", + ApiMappingId: "test_mapping_id", + ApiMappingKey: dc.basePath, + DomainName: dc.givenDomainName, + Stage: "$default", + }); + }); + + it("Remove basepath mappings", async () => { + AWS.mock("CloudFormation", "describeStackResource", (params, callback) => { + callback(null, { + StackResourceDetail: + { + LogicalResourceId: "ApiGatewayRestApi", + PhysicalResourceId: "test_rest_api_id", + }, + }); + }); + AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => { + callback(null, { + Items: [ + { ApiId: "test_rest_api_id", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test" }, + ], + }); + }); + AWS.mock("ApiGatewayV2", "deleteApiMapping", (params, callback) => { + callback(null, params); + }); + + const plugin = constructPlugin({ + basePath: "test_basepath", + domainName: "test_domain", + restApiId: "test_rest_api_id", + }); + plugin.initializeVariables(); + + plugin.apigatewayV2 = new aws.ApiGatewayV2(); + plugin.cloudformation = new aws.CloudFormation(); + + plugin.domains[0].apiMapping = {ApiMappingId: "test_mapping_id"}; + + const spy = chai.spy.on(plugin.apigatewayV2, "deleteApiMapping"); + + await plugin.removeBasePathMappings(); + expect(spy).to.have.been.called.with({ + ApiMappingId: "test_mapping_id", + DomainName: "test_domain", + }); + }); + it("Add Distribution Domain Name, Domain Name, and HostedZoneId to stack output", () => { const plugin = constructPlugin({ domainName: "test_domain", }); - plugin.addOutputs(new DomainInfo({ + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + dc.domainInfo = new DomainInfo({ distributionDomainName: "fake_dist_name", distributionHostedZoneId: "fake_zone_id", domainName: "fake_domain", - })); + }); + + plugin.addOutputs(dc); + const cfTemplat = plugin.serverless.service.provider.compiledCloudFormationTemplate.Outputs; expect(cfTemplat).to.not.equal(undefined); }); @@ -194,10 +352,14 @@ describe("Custom Domain Plugin", () => { }); plugin.initializeVariables(); plugin.apigateway = new aws.APIGateway(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + dc.apiId = "test_rest_api_id"; + const spy = chai.spy.on(plugin.apigateway, "createBasePathMapping"); - await plugin.createBasePathMapping("test_rest_api_id"); + await plugin.createBasePathMapping(dc); expect(spy).to.have.been.called.with({ basePath: "(none)", domainName: "test_domain", @@ -217,10 +379,14 @@ describe("Custom Domain Plugin", () => { }); plugin.initializeVariables(); plugin.apigateway = new aws.APIGateway(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + dc.apiId = "test_rest_api_id"; + const spy = chai.spy.on(plugin.apigateway, "createBasePathMapping"); - await plugin.createBasePathMapping("test_rest_api_id"); + await plugin.createBasePathMapping(dc); expect(spy).to.have.been.called.with({ basePath: "(none)", domainName: "test_domain", @@ -239,10 +405,14 @@ describe("Custom Domain Plugin", () => { }); plugin.initializeVariables(); plugin.apigateway = new aws.APIGateway(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + dc.apiId = "test_rest_api_id"; + const spy = chai.spy.on(plugin.apigateway, "createBasePathMapping"); - await plugin.createBasePathMapping("test_rest_api_id"); + await plugin.createBasePathMapping(dc); expect(spy).to.have.been.called.with({ basePath: "(none)", domainName: "test_domain", @@ -262,10 +432,14 @@ describe("Custom Domain Plugin", () => { plugin.initializeVariables(); plugin.cloudformation = new aws.CloudFormation(); plugin.apigateway = new aws.APIGateway(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + dc.apiId = "test_rest_api_id"; + const spy = chai.spy.on(plugin.apigateway, "createBasePathMapping"); - await plugin.createBasePathMapping("test_rest_api_id"); + await plugin.createBasePathMapping(dc); expect(spy).to.have.been.called.with({ basePath: "(none)", domainName: "test_domain", @@ -291,7 +465,9 @@ describe("Custom Domain Plugin", () => { const plugin = constructPlugin(options); plugin.acm = new aws.ACM(); - const result = await plugin.getCertArn(); + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + const result = await plugin.getCertArn(dc); expect(result).to.equal("test_given_arn"); }); @@ -302,7 +478,9 @@ describe("Custom Domain Plugin", () => { const plugin = constructPlugin({ certificateName: "cert_name" }); plugin.acm = new aws.ACM(); - const result = await plugin.getCertArn(); + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + const result = await plugin.getCertArn(dc); expect(result).to.equal("test_given_cert_name"); }); @@ -313,13 +491,36 @@ describe("Custom Domain Plugin", () => { }); const plugin = constructPlugin({ domainName: "test_domain"}); + plugin.initializeVariables(); plugin.apigateway = new aws.APIGateway(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; - const result = await plugin.createCustomDomain("fake_cert"); + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + dc.certificateArn = "fake_cert"; + + await plugin.createCustomDomain(dc); + + expect(dc.domainInfo.domainName).to.equal("foo"); + expect(dc.domainInfo.securityPolicy).to.equal("TLS_1_2"); + }); + + it("Create an HTTP domain name", async () => { + AWS.mock("ApiGatewayV2", "createDomainName", (params, callback) => { + callback(null, { DomainName: "foo", DomainNameConfigurations: [{SecurityPolicy: "TLS_1_2"}]}); + }); + + const plugin = constructPlugin({ domainName: "test_domain", apiType: "http", endpointType: "regional"}); + plugin.initializeVariables(); + plugin.apigatewayV2 = new aws.ApiGatewayV2(); + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + dc.certificateArn = "fake_cert"; - expect(result.domainName).to.equal("foo"); - expect(result.securityPolicy).to.equal("TLS_1_2"); + await plugin.createCustomDomain(dc); + + expect(dc.domainInfo.domainName).to.equal("foo"); + expect(dc.domainInfo.securityPolicy).to.equal("TLS_1_2"); }); it("Create a domain name with specific TLS version", async () => { @@ -328,13 +529,17 @@ describe("Custom Domain Plugin", () => { }); const plugin = constructPlugin({ domainName: "test_domain", securityPolicy: "tls_1_2"}); + plugin.initializeVariables(); plugin.apigateway = new aws.APIGateway(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; - const result = await plugin.createCustomDomain("fake_cert"); + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); - expect(result.domainName).to.equal("foo"); - expect(result.securityPolicy).to.equal("TLS_1_2"); + dc.certificateArn = "fake_cert"; + + await plugin.createCustomDomain(dc); + + expect(dc.domainInfo.domainName).to.equal("foo"); + expect(dc.domainInfo.securityPolicy).to.equal("TLS_1_2"); }); it("Create a new A Alias Record", async () => { @@ -346,19 +551,21 @@ describe("Custom Domain Plugin", () => { callback(null, params); }); - const plugin = constructPlugin({ basePath: "test_basepath" }); + const plugin = constructPlugin({ basePath: "test_basepath", domainName: "test_domain" }); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = "test_domain"; - const spy = chai.spy.on(plugin.route53, "changeResourceRecordSets"); - const domain = new DomainInfo( + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + dc.domainInfo = new DomainInfo( { distributionDomainName: "test_distribution_name", distributionHostedZoneId: "test_id", }, ); - await plugin.changeResourceRecordSet("UPSERT", domain); + const spy = chai.spy.on(plugin.route53, "changeResourceRecordSets"); + + await plugin.changeResourceRecordSet("UPSERT", dc); const expectedParams = { ChangeBatch: { @@ -400,7 +607,10 @@ describe("Custom Domain Plugin", () => { createRoute53Record: false, domainName: "test_domain", }); - const result = await plugin.changeResourceRecordSet("UPSERT", new DomainInfo({})); + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + const result = await plugin.changeResourceRecordSet("UPSERT", dc); expect(result).to.equal(undefined); }); @@ -411,11 +621,11 @@ describe("Custom Domain Plugin", () => { }); describe("Gets existing basepath mappings correctly", () => { - it("Returns undefined if no basepaths map to current restApiId", async () => { - AWS.mock("APIGateway", "getBasePathMappings", (params, callback) => { + it("Returns undefined if no basepaths map to current api", async () => { + AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => { callback(null, { - items: [ - { basePath: "(none)", restApiId: "test_rest_api_id_one", stage: "test" }, + Items: [ + { ApiId: "someother_api_id", MappingKey: "test", ApiMappingId: "test_rest_api_id_one", Stage: "test" }, ], }); }); @@ -423,33 +633,42 @@ describe("Custom Domain Plugin", () => { const plugin = constructPlugin({ domainName: "test_domain", }); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; - plugin.basePath = plugin.serverless.service.custom.customDomain.basePath; + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + dc.apiMapping = {ApiMappingId: "api_id"}; + plugin.initializeVariables(); - const result = await plugin.getBasePathMapping("test_rest_api_id_two"); + const result = await plugin.getBasePathMapping(dc); expect(result).to.equal(undefined); }); - it("Returns current api", async () => { - AWS.mock("APIGateway", "getBasePathMappings", (params, callback) => { + it("Returns current api mapping", async () => { + AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => { callback(null, { - items: [ - { basePath: "api", restApiId: "test_rest_api_id", stage: "test" }, + Items: [ + { ApiId: "test_rest_api_id", ApiMappingKey: "api", ApiMappingId: "fake_id", Stage: "test" }, ], }); }); const plugin = constructPlugin({ + apiType: Globals.apiTypes.rest, basePath: "api", domainName: "test_domain", }); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; - plugin.basePath = plugin.serverless.service.custom.customDomain.basePath; + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + dc.apiId = "test_rest_api_id"; + plugin.initializeVariables(); - const result = await plugin.getBasePathMapping("test_rest_api_id"); - expect(result).to.equal("api"); + const result = await plugin.getBasePathMapping(dc); + expect(result).to.eql({ + ApiId: "test_rest_api_id", + ApiMappingId: "fake_id", + ApiMappingKey: "api", + Stage: "test" }); }); afterEach(() => { @@ -458,8 +677,8 @@ describe("Custom Domain Plugin", () => { }); }); - describe("Gets Rest API correctly", () => { - it("Fetches restApiId correctly when no ApiGateway specified", async () => { + describe("Gets Rest API id correctly", () => { + it("Gets REST API id correctly when no ApiGateway specified", async () => { AWS.mock("CloudFormation", "describeStackResource", (params, callback) => { callback(null, { StackResourceDetail: @@ -473,10 +692,82 @@ describe("Custom Domain Plugin", () => { basePath: "test_basepath", domainName: "test_domain", }); + plugin.initializeVariables(); plugin.cloudformation = new aws.CloudFormation(); - const result = await plugin.getRestApiId(); + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + const spy = chai.spy.on(plugin.cloudformation, "describeStackResource"); + + const result = await plugin.getApiId(dc); + expect(result).to.equal("test_rest_api_id"); + expect(spy).to.have.been.called.with({ + LogicalResourceId: "ApiGatewayRestApi", + StackName: "custom-stage-name", + }); + }); + + it("Gets HTTP API id correctly when no ApiGateway specified", async () => { + AWS.mock("CloudFormation", "describeStackResource", (params, callback) => { + callback(null, { + StackResourceDetail: + { + LogicalResourceId: "HttpApi", + PhysicalResourceId: "test_http_api_id", + }, + }); + }); + const plugin = constructPlugin({ + apiType: "http", + basePath: "test_basepath", + domainName: "test_domain", + endpointType: "regional", + }); + plugin.initializeVariables(); + plugin.cloudformation = new aws.CloudFormation(); + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + const spy = chai.spy.on(plugin.cloudformation, "describeStackResource"); + + const result = await plugin.getApiId(dc); + expect(result).to.equal("test_http_api_id"); + expect(spy).to.have.been.called.with({ + LogicalResourceId: "HttpApi", + StackName: "custom-stage-name", + }); + }); + + it("Gets Websocket API id correctly when no ApiGateway specified", async () => { + AWS.mock("CloudFormation", "describeStackResource", (params, callback) => { + callback(null, { + StackResourceDetail: + { + LogicalResourceId: "WebsocketsApi", + PhysicalResourceId: "test_ws_api_id", + }, + }); + }); + const plugin = constructPlugin({ + apiType: "websocket", + basePath: "test_basepath", + domainName: "test_domain", + endpointType: "regional", + }); + plugin.initializeVariables(); + plugin.cloudformation = new aws.CloudFormation(); + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + const spy = chai.spy.on(plugin.cloudformation, "describeStackResource"); + + const result = await plugin.getApiId(dc); + expect(result).to.equal("test_ws_api_id"); + expect(spy).to.have.been.called.with({ + LogicalResourceId: "WebsocketsApi", + StackName: "custom-stage-name", + }); }); it("serverless.yml defines explicitly the apiGateway", async () => { @@ -497,11 +788,14 @@ describe("Custom Domain Plugin", () => { plugin.cloudformation = new aws.CloudFormation(); plugin.serverless.service.provider.apiGateway.restApiId = "custom_test_rest_api_id"; - const result = await plugin.getRestApiId(); + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + const result = await plugin.getApiId(dc); expect(result).to.equal("custom_test_rest_api_id"); }); afterEach(() => { + AWS.restore(); consoleOutput = []; }); }); @@ -517,11 +811,12 @@ describe("Custom Domain Plugin", () => { domainName: "test_domain", }); plugin.apigateway = new aws.APIGateway(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; - const result = await plugin.getDomainInfo(); + await plugin.getDomainInfo(); - expect(result.domainName).to.equal("test_domain"); + plugin.domains.forEach((domain) => { + expect(domain.domainInfo.domainName).to.equal("test_domain"); + }); }); it("Delete A Alias Record", async () => { @@ -538,15 +833,17 @@ describe("Custom Domain Plugin", () => { domainName: "test_domain", }); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + const spy = chai.spy.on(plugin.route53, "changeResourceRecordSets"); - const domain = new DomainInfo({ + dc.domainInfo = new DomainInfo({ distributionDomainName: "test_distribution_name", distributionHostedZoneId: "test_id", }); - await plugin.changeResourceRecordSet("DELETE", domain); + await plugin.changeResourceRecordSet("DELETE", dc); const expectedParams = { ChangeBatch: { Changes: [ @@ -584,7 +881,7 @@ describe("Custom Domain Plugin", () => { }); it("Delete the domain name", async () => { - AWS.mock("APIGateway", "deleteDomainName", (params, callback) => { + AWS.mock("ApiGatewayV2", "deleteDomainName", (params, callback) => { callback(null, {}); }); @@ -592,13 +889,15 @@ describe("Custom Domain Plugin", () => { basePath: "test_basepath", domainName: "test_domain", }); - plugin.apigateway = new aws.APIGateway(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; - const spy = chai.spy.on(plugin.apigateway, "deleteDomainName"); + plugin.apigatewayV2 = new aws.ApiGatewayV2(); + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + const spy = chai.spy.on(plugin.apigatewayV2, "deleteDomainName"); - await plugin.deleteCustomDomain(); + await plugin.deleteCustomDomain(dc); expect(spy).to.be.called.with({ - domainName: "test_domain", + DomainName: "test_domain", }); }); @@ -610,11 +909,12 @@ describe("Custom Domain Plugin", () => { describe("Hook Methods", () => { it("setupBasePathMapping", async () => { - AWS.mock("APIGateway", "getDomainName", (params, callback) => { - callback(null, { domainName: "fake_domain", distributionDomainName: "fake_dist_name" }); + AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => { + callback(null, {DomainName: "test_domain", + DomainNameConfigurations: [{ApiGatewayDomainName: "fake_dist_name"}]}); }); - AWS.mock("APIGateway", "getBasePathMappings", (params, callback) => { - callback(null, { items: [] }); + AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => { + callback(null, { Items: [] }); }); AWS.mock("APIGateway", "createBasePathMapping", (params, callback) => { callback(null, params); @@ -631,20 +931,20 @@ describe("Custom Domain Plugin", () => { const plugin = constructPlugin({ domainName: "test_domain"}); plugin.initializeVariables(); plugin.apigateway = new aws.APIGateway(); + plugin.apigatewayV2 = new aws.ApiGatewayV2(); plugin.cloudformation = new aws.CloudFormation(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; const spy = chai.spy.on(plugin, "createBasePathMapping"); - await plugin.setupBasePathMapping(); + await plugin.setupBasePathMappings(); expect(spy).to.be.called(); }); it("deleteDomain", async () => { - AWS.mock("APIGateway", "getDomainName", (params, callback) => { - callback(null, { distributionDomainName: "test_distribution", regionalHostedZoneId: "test_id" }); + AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => { + callback(null, {DomainName: "test_domain", DomainNameConfigurations: [{HostedZoneId: "test_id"}]}); }); - AWS.mock("APIGateway", "deleteDomainName", (params, callback) => { + AWS.mock("ApiGatewayV2", "deleteDomainName", (params, callback) => { callback(null, {}); }); AWS.mock("Route53", "listHostedZones", (params, callback) => { @@ -657,14 +957,15 @@ describe("Custom Domain Plugin", () => { const plugin = constructPlugin({ domainName: "test_domain"}); plugin.apigateway = new aws.APIGateway(); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; - await plugin.deleteDomain(); - expect(consoleOutput[0]).to.equal(`Custom domain ${plugin.givenDomainName} was deleted.`); + plugin.initializeVariables(); + + await plugin.deleteDomains(); + expect(consoleOutput[0]).to.equal(`Custom domain ${plugin.domains[0].givenDomainName} was deleted.`); }); it("createDomain if one does not exist before", async () => { AWS.mock("ACM", "listCertificates", certTestData); - AWS.mock("APIGateway", "getDomainName", (params, callback) => { + AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => { callback({ code: "NotFoundException" }, {}); }); AWS.mock("APIGateway", "createDomainName", (params, callback) => { @@ -681,16 +982,18 @@ describe("Custom Domain Plugin", () => { plugin.apigateway = new aws.APIGateway(); plugin.route53 = new aws.Route53(); plugin.acm = new aws.ACM(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; - await plugin.createDomain(); - expect(consoleOutput[0]).to.equal(`Custom domain ${plugin.givenDomainName} was created. - New domains may take up to 40 minutes to be initialized.`); + + plugin.initializeVariables(); + await plugin.createDomains(); + + expect(consoleOutput[0]).to.equal(`Custom domain ${plugin.domains[0].givenDomainName} was created. + New domains may take up to 40 minutes to be initialized.`); }); it("Does not create domain if one existed before", async () => { AWS.mock("ACM", "listCertificates", certTestData); - AWS.mock("APIGateway", "getDomainName", (params, callback) => { - callback(null, { distributionDomainName: "foo", regionalHostedZoneId: "test_id" }); + AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => { + callback(null, {DomainName: "test_domain", DomainNameConfigurations: [{HostedZoneId: "test_id"}]}); }); AWS.mock("APIGateway", "createDomainName", (params, callback) => { callback(null, { distributionDomainName: "foo", regionalHostedZoneId: "test_id" }); @@ -706,9 +1009,9 @@ describe("Custom Domain Plugin", () => { plugin.apigateway = new aws.APIGateway(); plugin.route53 = new aws.Route53(); plugin.acm = new aws.ACM(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; - await plugin.createDomain(); - expect(consoleOutput[0]).to.equal(`Custom domain ${plugin.givenDomainName} already exists.`); + plugin.initializeVariables(); + await plugin.createDomains(); + expect(consoleOutput[0]).to.equal(`Custom domain test_domain already exists.`); }); afterEach(() => { @@ -730,11 +1033,10 @@ describe("Custom Domain Plugin", () => { }); }); - const plugin = constructPlugin({}); + const plugin = constructPlugin({domainName: "ccc.bbb.aaa.com"}); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = "ccc.bbb.aaa.com"; - - const result = await plugin.getRoute53HostedZoneId(); + plugin.initializeVariables(); + const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]); expect(result).to.equal("test_id_2"); }); @@ -750,11 +1052,11 @@ describe("Custom Domain Plugin", () => { }); }); - const plugin = constructPlugin({}); + const plugin = constructPlugin({domainName: "test.ccc.bbb.aaa.com"}); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = "test.ccc.bbb.aaa.com"; + plugin.initializeVariables(); - const result = await plugin.getRoute53HostedZoneId(); + const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]); expect(result).to.equal("test_id_1"); }); @@ -770,11 +1072,11 @@ describe("Custom Domain Plugin", () => { }); }); - const plugin = constructPlugin({}); + const plugin = constructPlugin({domainName: "test.ccc.bbb.aaa.com"}); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = "test.ccc.bbb.aaa.com"; + plugin.initializeVariables(); - const result = await plugin.getRoute53HostedZoneId(); + const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]); expect(result).to.equal("test_id_2"); }); @@ -789,11 +1091,11 @@ describe("Custom Domain Plugin", () => { }); }); - const plugin = constructPlugin({}); + const plugin = constructPlugin({domainName: "bar.foo.bbb.fr"}); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = "bar.foo.bbb.fr"; + plugin.initializeVariables(); - const result = await plugin.getRoute53HostedZoneId(); + const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]); expect(result).to.equal("test_id_1"); }); @@ -807,11 +1109,11 @@ describe("Custom Domain Plugin", () => { }); }); - const plugin = constructPlugin({}); + const plugin = constructPlugin({domainName: "test.a.aaa.com"}); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = "test.a.aaa.com"; + plugin.initializeVariables(); - const result = await plugin.getRoute53HostedZoneId(); + const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]); expect(result).to.equal("test_id_0"); }); @@ -827,11 +1129,11 @@ describe("Custom Domain Plugin", () => { }); }); - const plugin = constructPlugin({}); + const plugin = constructPlugin({domainName: "bar.foo.bbb.fr"}); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = "bar.foo.bbb.fr"; + plugin.initializeVariables(); - const result = await plugin.getRoute53HostedZoneId(); + const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]); expect(result).to.equal("test_id_3"); }); @@ -847,11 +1149,11 @@ describe("Custom Domain Plugin", () => { }); }); - const plugin = constructPlugin({}); + const plugin = constructPlugin({domainName: "bar.foo.bbb.fr"}); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = "bar.foo.bbb.fr"; + plugin.initializeVariables(); - const result = await plugin.getRoute53HostedZoneId(); + const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]); expect(result).to.equal("test_id_3"); }); @@ -866,11 +1168,11 @@ describe("Custom Domain Plugin", () => { }); }); - const plugin = constructPlugin({}); + const plugin = constructPlugin({domainName: "bar.foo.bbb.fr"}); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = "bar.foo.bbb.fr"; + plugin.initializeVariables(); - const result = await plugin.getRoute53HostedZoneId(); + const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]); expect(result).to.equal("test_id_3"); }); @@ -883,12 +1185,11 @@ describe("Custom Domain Plugin", () => { }); }); - const plugin = constructPlugin({}); + const plugin = constructPlugin({domainName: "aaa.com", hostedZonePrivate: true}); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = "aaa.com"; - plugin.hostedZonePrivate = true; + plugin.initializeVariables(); - const result = await plugin.getRoute53HostedZoneId(); + const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]); expect(result).to.equal("test_id_0"); }); @@ -901,11 +1202,11 @@ describe("Custom Domain Plugin", () => { }); }); - const plugin = constructPlugin({}); + const plugin = constructPlugin({domainName: "aaa.com"}); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = "aaa.com"; + plugin.initializeVariables(); - const result = await plugin.getRoute53HostedZoneId(); + const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]); expect(result).to.equal("test_id_0"); }); @@ -925,8 +1226,9 @@ describe("Custom Domain Plugin", () => { }; const plugin = constructPlugin(options); plugin.acm = new aws.ACM(); + plugin.initializeVariables(); - return plugin.getCertArn().then(() => { + return plugin.getCertArn(plugin.domains[0]).then(() => { throw new Error("Test has failed. getCertArn did not catch errors."); }).catch((err) => { const expectedErrorMessage = "Error: Could not find the certificate does_not_exist."; @@ -941,9 +1243,9 @@ describe("Custom Domain Plugin", () => { const plugin = constructPlugin({ domainName: "test_domain"}); plugin.route53 = new aws.Route53(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; + plugin.initializeVariables(); - return plugin.getRoute53HostedZoneId().then(() => { + return plugin.getRoute53HostedZoneId(plugin.domains[0]).then(() => { throw new Error("Test has failed, getHostedZone did not catch errors."); }).catch((err) => { const expectedErrorMessage = "Error: Could not find hosted zone \"test_domain\""; @@ -957,9 +1259,9 @@ describe("Custom Domain Plugin", () => { }); const plugin = constructPlugin({ domainName: "test_domain"}); plugin.apigateway = new aws.APIGateway(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; + plugin.initializeVariables(); - return plugin.domainSummary().then(() => { + return plugin.domainSummaries().then(() => { // check if distribution domain name is printed }).catch((err) => { const expectedErrorMessage = `Error: Unable to fetch information about test_domain`; @@ -969,17 +1271,17 @@ describe("Custom Domain Plugin", () => { it("Should log if SLS_DEBUG is set", async () => { const plugin = constructPlugin({ domainName: "test_domain" }); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; + plugin.initializeVariables(); // set sls debug to true process.env.SLS_DEBUG = "True"; plugin.logIfDebug("test message"); - expect(consoleOutput).to.contain("test message"); + expect(consoleOutput[0]).to.contain("test message"); }); it("Should not log if SLS_DEBUG is not set", async () => { const plugin = constructPlugin({ domainName: "test_domain" }); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; + plugin.initializeVariables(); plugin.logIfDebug("test message"); expect(consoleOutput).to.not.contain("test message"); @@ -994,19 +1296,18 @@ describe("Custom Domain Plugin", () => { describe("Summary Printing", () => { it("Prints Summary", async () => { - AWS.mock("APIGateway", "getDomainName", (params, callback) => { + AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => { callback(null, { domainName: params, distributionDomainName: "test_distributed_domain_name" }); }); const plugin = constructPlugin({domainName: "test_domain"}); - plugin.apigateway = new aws.APIGateway(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; + plugin.apigatewayV2 = new aws.ApiGatewayV2(); + plugin.initializeVariables(); - await plugin.domainSummary(); + await plugin.domainSummaries(); expect(consoleOutput[0]).to.contain("Serverless Domain Manager Summary"); - expect(consoleOutput[1]).to.contain("Domain Name"); + expect(consoleOutput[1]).to.contain("Distribution Domain Name"); expect(consoleOutput[2]).to.contain("test_domain"); - expect(consoleOutput[3]).to.contain("Distribution Domain Name"); - expect(consoleOutput[4]).to.contain("test_distributed_domain_name"); + expect(consoleOutput[3]).to.contain("test_distributed_domain_name"); }); afterEach(() => { @@ -1024,7 +1325,10 @@ describe("Custom Domain Plugin", () => { const returnedCreds = plugin.apigateway.config.credentials; expect(returnedCreds.accessKeyId).to.equal(testCreds.accessKeyId); expect(returnedCreds.sessionToken).to.equal(testCreds.sessionToken); - expect(plugin.enabled).to.equal(true); + expect(plugin.domains).length.to.be.greaterThan(0); + for (const domain of plugin.domains) { + expect(domain.enabled).to.equal(true); + } }); it("Should enable the plugin when passing a true parameter with type boolean", () => { @@ -1035,7 +1339,10 @@ describe("Custom Domain Plugin", () => { const returnedCreds = plugin.apigateway.config.credentials; expect(returnedCreds.accessKeyId).to.equal(testCreds.accessKeyId); expect(returnedCreds.sessionToken).to.equal(testCreds.sessionToken); - expect(plugin.enabled).to.equal(true); + expect(plugin.domains).length.to.be.greaterThan(0); + for (const domain of plugin.domains) { + expect(domain.enabled).to.equal(true); + } }); it("Should enable the plugin when passing a true parameter with type string", () => { @@ -1046,7 +1353,10 @@ describe("Custom Domain Plugin", () => { const returnedCreds = plugin.apigateway.config.credentials; expect(returnedCreds.accessKeyId).to.equal(testCreds.accessKeyId); expect(returnedCreds.sessionToken).to.equal(testCreds.sessionToken); - expect(plugin.enabled).to.equal(true); + expect(plugin.domains).length.to.be.greaterThan(0); + for (const domain of plugin.domains) { + expect(domain.enabled).to.equal(true); + } }); it("Should disable the plugin when passing a false parameter with type boolean", () => { @@ -1054,7 +1364,7 @@ describe("Custom Domain Plugin", () => { plugin.initializeVariables(); - expect(plugin.enabled).to.equal(false); + expect(plugin.domains.length).to.equal(0); }); it("Should disable the plugin when passing a false parameter with type string", () => { @@ -1062,56 +1372,50 @@ describe("Custom Domain Plugin", () => { plugin.initializeVariables(); - expect(plugin.enabled).to.equal(false); + expect(plugin.domains.length).to.equal(0); }); it("createDomain should do nothing when domain manager is disabled", async () => { const plugin = constructPlugin({ enabled: false }); - const result = await plugin.hookWrapper(plugin.createDomain); + await plugin.hookWrapper(plugin.createDomains); - expect(plugin.enabled).to.equal(false); - expect(result).to.equal(undefined); + expect(plugin.domains.length).to.equal(0); }); it("deleteDomain should do nothing when domain manager is disabled", async () => { const plugin = constructPlugin({ enabled: false }); - const result = await plugin.hookWrapper(plugin.deleteDomain); + await plugin.hookWrapper(plugin.deleteDomains); - expect(plugin.enabled).to.equal(false); - expect(result).to.equal(undefined); + expect(plugin.domains.length).to.equal(0); }); it("setUpBasePathMapping should do nothing when domain manager is disabled", async () => { const plugin = constructPlugin({ enabled: false }); - const result = await plugin.hookWrapper(plugin.setupBasePathMapping); + await plugin.hookWrapper(plugin.setupBasePathMappings); - expect(plugin.enabled).to.equal(false); - expect(result).to.equal(undefined); + expect(plugin.domains.length).to.equal(0); }); it("removeBasePathMapping should do nothing when domain manager is disabled", async () => { const plugin = constructPlugin({ enabled: false }); - const result = await plugin.hookWrapper(plugin.removeBasePathMapping); + await plugin.hookWrapper(plugin.removeBasePathMappings); - expect(plugin.enabled).to.equal(false); - expect(result).to.equal(undefined); + expect(plugin.domains.length).to.equal(0); }); it("domainSummary should do nothing when domain manager is disabled", async () => { const plugin = constructPlugin({ enabled: false }); - const result = await plugin.hookWrapper(plugin.domainSummary); + await plugin.hookWrapper(plugin.domainSummaries); - expect(plugin.enabled).to.equal(false); - expect(result).to.equal(undefined); + expect(plugin.domains.length).to.equal(0); }); it("Should throw an Error when passing a parameter that is not boolean", () => { - const stringWithValueYes = "yes"; const plugin = constructPlugin({ enabled: 0 }); let errored = false; diff --git a/types.ts b/types.ts index 5d612493..0e8c089c 100644 --- a/types.ts +++ b/types.ts @@ -20,6 +20,7 @@ export interface ServerlessInstance { // tslint:disable-line certificateArn: string | undefined, createRoute53Record: boolean | undefined, endpointType: string | undefined, + apiType: string | undefined, hostedZoneId: string | undefined, hostedZonePrivate: boolean | undefined, enabled: boolean | string | undefined, @@ -31,6 +32,7 @@ export interface ServerlessInstance { // tslint:disable-line aws: { sdk: { APIGateway: any, + ApiGatewayV2: any, Route53: any, CloudFormation: any, ACM: any, From 7f6ef9350e36104818980cbcfd374f150efc026f Mon Sep 17 00:00:00 2001 From: Jason Venable Date: Fri, 1 May 2020 23:31:41 -0400 Subject: [PATCH 2/5] Fix typos and clean up comments --- README.md | 4 ++-- index.ts | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a3b12207..c2680eaf 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ plugins: - serverless-domain-manager ``` -Add the plugin configuration (example for `serverless.foo.com/api`). For a single domain and API type the following sturcture can be used. +Add the plugin configuration (example for `serverless.foo.com/api`). For a single domain and API type the following structure can be used. ```yaml custom: @@ -118,7 +118,7 @@ custom: | hostedZonePrivate | | If hostedZonePrivate is set to `true` then only private hosted zones will be used for route 53 records. If it is set to `false` then only public hosted zones will be used for route53 records. Setting this parameter is specially useful if you have multiple hosted zones with the same domain name (e.g. a public and a private one) | | enabled | true | Sometimes there are stages for which is not desired to have custom domain names. This flag allows the developer to disable the plugin for such cases. Accepts either `boolean` or `string` values and defaults to `true` for backwards compatibility. | securityPolicy | tls_1_2 | The security policy to apply to the custom domain name. Accepts `tls_1_0` or `tls_1_2`| -allowPathMatching | false | When updating an existing api mapping this will match on the basePath instead of the API ID to find existing mappings for an udpate. This should ony be used when changing API types. For example, migrating a REST API to an HTTP API. See Changing API Types for moe information. | +allowPathMatching | false | When updating an existing api mapping this will match on the basePath instead of the API ID to find existing mappings for an upsate. This should only be used when changing API types. For example, migrating a REST API to an HTTP API. See Changing API Types for more information. | ## Running diff --git a/index.ts b/index.ts index f3b034ef..96a48ee6 100644 --- a/index.ts +++ b/index.ts @@ -119,7 +119,7 @@ class ServerlessCustomDomain { domain.domainInfo = undefined; this.serverless.cli.log(`Custom domain ${domain.givenDomainName} was deleted.`); } else { - this.serverless.cli.log(`Custom domain ${domain.givenDomainName} does not exists.`); + this.serverless.cli.log(`Custom domain ${domain.givenDomainName} does not exist.`); } } catch (err) { this.logIfDebug(err, domain.givenDomainName); @@ -158,7 +158,6 @@ class ServerlessCustomDomain { } await this.getDomainInfo(); - // this.addOutputs(domain); } catch (err) { this.logIfDebug(err, domain.givenDomainName); @@ -181,7 +180,7 @@ class ServerlessCustomDomain { try { domain.apiId = await this.getApiId(domain); - // Unable to find the correspond API, manuall clean up will be required + // Unable to find the corresponding API, manual clean up will be required if (!domain.apiId) { this.serverless.cli.log(`Unable to find corresponding API for ${domain.givenDomainName}, API Mappings may need to be manually removed.`, "Serverless Domain Manager"); @@ -403,7 +402,7 @@ class ServerlessCustomDomain { // Make API call to create domain try { - // If creating REST api use v1 of api gateway, else use v2 for HTTP and Websocket + // Creating EDGE domain so use APIGateway (v1) service createdDomain = await this.apigateway.createDomainName(params).promise(); domain.domainInfo = new DomainInfo(createdDomain); } catch (err) { @@ -423,7 +422,7 @@ class ServerlessCustomDomain { // Make API call to create domain try { - // If creating REST api use v1 of api gateway, else use v2 for HTTP and Websocket + // Creating Regional domain so use ApiGatewayV2 createdDomain = await this.apigatewayV2.createDomainName(params).promise(); domain.domainInfo = new DomainInfo(createdDomain); } catch (err) { From df0d80b171f2455ff56284037097720bdfd5c18a Mon Sep 17 00:00:00 2001 From: Jason Venable Date: Fri, 1 May 2020 23:40:37 -0400 Subject: [PATCH 3/5] Make evaluateEnabled() in DomainConfig private --- DomainConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DomainConfig.ts b/DomainConfig.ts index 5a67ed01..1b707a2e 100644 --- a/DomainConfig.ts +++ b/DomainConfig.ts @@ -81,7 +81,7 @@ class DomainConfig { * If the property's value is provided, this should be boolean, otherwise an exception is thrown. * If no customDomain object exists, an exception is thrown. */ - public evaluateEnabled(enabled: any): boolean { + private evaluateEnabled(enabled: any): boolean { // const enabled = this.serverless.service.custom.customDomain.enabled; if (enabled === undefined) { return true; From 5a87e435b2e28be82ba2d637941bcb440871ea73 Mon Sep 17 00:00:00 2001 From: Jason Venable Date: Mon, 4 May 2020 23:08:14 -0400 Subject: [PATCH 4/5] Fix more typos --- index.ts | 10 +++++----- test/unit-tests/index.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/index.ts b/index.ts index 96a48ee6..535eaf84 100644 --- a/index.ts +++ b/index.ts @@ -98,7 +98,7 @@ class ServerlessCustomDomain { } } catch (err) { this.logIfDebug(err, domain.givenDomainName); - throw new Error(`Error: Unable to craete domain ${domain.givenDomainName}`); + throw new Error(`Error: Unable to create domain ${domain.givenDomainName}`); } })); } @@ -238,7 +238,7 @@ class ServerlessCustomDomain { this.route53 = new this.serverless.providers.aws.sdk.Route53(credentials); this.cloudformation = new this.serverless.providers.aws.sdk.CloudFormation(credentials); - // Loop over the domain configurations and popluates the domains array with DomainConfigs + // Loop over the domain configurations and populate the domains array with DomainConfigs this.domains = []; // If the key of the item in config is an api type it is using per api type domain structure @@ -248,7 +248,7 @@ class ServerlessCustomDomain { this.serverless.service.custom.customDomain[configApiType].apiType = configApiType; this.domains.push(new DomainConfig(this.serverless.service.custom.customDomain[configApiType])); } else { - throw Error(`Error: Invalud API Type, ${configApiType}`); + throw Error(`Error: Invalid API Type, ${configApiType}`); } } } else { // Default to single domain config @@ -266,7 +266,7 @@ class ServerlessCustomDomain { this.acm = new this.serverless.providers.aws.sdk.ACM(acmCredentials); } - // Validate the domain configuraitons + // Validate the domain configurations this.validateDomainConfigs(); } @@ -309,7 +309,7 @@ class ServerlessCustomDomain { return domain.certificateArn; } - let certificateArn; // The arn of the choosen certificate + let certificateArn; // The arn of the selected certificate let certificateName = domain.certificateName; // The certificate name diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts index cf310f96..803a2e61 100644 --- a/test/unit-tests/index.test.ts +++ b/test/unit-tests/index.test.ts @@ -337,8 +337,8 @@ describe("Custom Domain Plugin", () => { plugin.addOutputs(dc); - const cfTemplat = plugin.serverless.service.provider.compiledCloudFormationTemplate.Outputs; - expect(cfTemplat).to.not.equal(undefined); + const cfTemplate = plugin.serverless.service.provider.compiledCloudFormationTemplate.Outputs; + expect(cfTemplate).to.not.equal(undefined); }); it("(none) is added if basepath is an empty string", async () => { From 3eca83b8b56cf54d9ac2b1de58a844a9377436ca Mon Sep 17 00:00:00 2001 From: Jason Venable Date: Mon, 4 May 2020 23:45:45 -0400 Subject: [PATCH 5/5] =?UTF-8?q?Don=E2=80=99t=20throw=20hard=20error=20if?= =?UTF-8?q?=20base=20path=20mappings=20can=E2=80=99t=20be=20removed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.ts b/index.ts index 535eaf84..06e05002 100644 --- a/index.ts +++ b/index.ts @@ -194,7 +194,8 @@ class ServerlessCustomDomain { API Mappings may need to be manually removed.`, "Serverless Domain Manager"); } else { this.logIfDebug(err, domain.givenDomainName); - throw new Error(`Error: Unable to remove base bath mappings for domain ${domain.givenDomainName}`); + this.serverless.cli.log(`Error: Unable to remove base bath mappings + for domain ${domain.givenDomainName}`); } } }));