diff --git a/DomainConfig.ts b/DomainConfig.ts new file mode 100644 index 00000000..9a1f96a6 --- /dev/null +++ b/DomainConfig.ts @@ -0,0 +1,98 @@ +/** + * Wrapper class for Custom Domain information + */ + +import Globals from "./Globals"; +import DomainInfo = require("./DomainInfo"); + +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 apiId: string | undefined; + public domainInfo: DomainInfo | undefined; + public currentBasePath: string | undefined; + public apiMappingId: string | undefined; + + 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; + + 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..e4aedd84 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,6 +113,7 @@ 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. | diff --git a/index.ts b/index.ts index 9297286c..246e95ec 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,11 +51,11 @@ class ServerlessCustomDomain { }, }; this.hooks = { - "after:deploy:deploy": this.hookWrapper.bind(this, this.setupBasePathMapping), - "after:info:info": this.hookWrapper.bind(this, this.domainSummary), - "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), + "after:deploy:deploy": this.hookWrapper.bind(this, this.setupBasePathMappings), + "after:info:info": this.hookWrapper.bind(this, this.domainSummaries), + "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), }; } @@ -74,192 +64,194 @@ 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 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(); - this.addOutputs(domainInfo); - await this.printDomainSummary(domainInfo); + public async setupBasePathMappings(): Promise { + await Promise.all(this.domains.map(async (domain) => { + try { + domain.apiId = await this.getApiId(domain); + + if (domain.apiType === Globals.apiTypes.rest) { + domain.currentBasePath = await this.getBasePathMapping(domain); + }else if (domain.apiType === Globals.apiTypes.http || domain.apiType === Globals.apiTypes.websocket) { + domain.apiMappingId = await this.getBasePathMapping(domain); + } + + if (!domain.currentBasePath && !domain.apiMappingId) { + await this.createBasePathMapping(domain); + } else { + await this.updateBasePathMapping(domain); + } + + await this.getDomainInfo(); + this.addOutputs(domain); + this.printDomainSummary(domain); + + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Unable to setup base domain mappings for ${domain.givenDomainName}`); + } + })); } /** * 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 { + await this.deleteBasePathMapping(domain); + } catch (err) { + 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; - 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.securityPolicy = tlsVersionToUse; - - this.acmRegion = this.endpointType === 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); - } - } - - /** - * 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(): boolean { + // 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 enabled = this.serverless.service.custom.customDomain.enabled; - if (enabled === undefined) { - return true; + 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.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}`); + } + } + } else { // Default to single domain config + this.domains.push(new DomainConfig(this.serverless.service.custom.customDomain)); } - if (typeof enabled === "boolean") { - return enabled; - } else if (typeof enabled === "string" && enabled === "true") { - return true; - } else if (typeof enabled === "string" && enabled === "false") { - return false; + + // 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); } - throw new Error(`serverless-domain-manager: Ambiguous enablement boolean: "${enabled}"`); } /** * 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 let certData; try { certData = await this.acm.listCertificates( @@ -276,7 +268,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 @@ -293,7 +285,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) { @@ -303,68 +295,98 @@ 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.apigateway.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`); + + // Gateway API is completely different for v1 and v2 so seperating into two blocks + if (domain.apiType === Globals.apiTypes.rest) { + // Set up parameters + const params = { + certificateArn: domain.certificateArn, + domainName: domain.givenDomainName, + endpointConfiguration: { + types: [domain.endpointType], + }, + regionalCertificateArn: domain.certificateArn, + securityPolicy: domain.securityPolicy, + }; + + if (domain.endpointType === Globals.endpointTypes.edge) { + params.regionalCertificateArn = undefined; + } else if (domain.endpointType === Globals.endpointTypes.regional) { + params.certificateArn = undefined; + } + + // 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 if (domain.apiType === Globals.apiTypes.http || domain.apiType === Globals.apiTypes.websocket) { + 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 { + public async deleteCustomDomain(domain: DomainConfig): Promise { const params = { - domainName: this.givenDomainName, + domainName: domain.givenDomainName, }; // Make API call try { await this.apigateway.deleteDomainName(params).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`); } } @@ -373,28 +395,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."); 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, }, })); @@ -409,30 +431,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(); @@ -444,7 +466,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 @@ -470,93 +492,164 @@ 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 { - const params = { - domainName: this.givenDomainName, - }; + public async getBasePathMapping(domain: DomainConfig): Promise { 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; + + if (domain.apiType === Globals.apiTypes.rest) { + const params = { + domainName: domain.givenDomainName, + }; + try { + basepathInfo = await this.apigateway.getBasePathMappings(params).promise(); + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Unable to get BasePathMappings for ${domain.givenDomainName}`); + } + if (basepathInfo.items !== undefined && basepathInfo.items instanceof Array) { + for (const basepathObj of basepathInfo.items) { + if (basepathObj.restApiId === domain.apiId) { + return basepathObj.basePath; + } + } + } + + } else if (domain.apiType === Globals.apiTypes.http + || domain.apiType === Globals.apiTypes.websocket) { // V2 HTTP and WEBSOCKET + + const params = { + DomainName: domain.givenDomainName, + }; + try { + basepathInfo = await this.apigatewayV2.getApiMappings(params).promise(); + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Unable to get BasePathMappings for ${domain.givenDomainName}`); + } + if (basepathInfo.Items !== undefined && basepathInfo.Items instanceof Array) { + for (const basepathObj of basepathInfo.Items) { + if (basepathObj.ApiId === domain.apiId) { + return basepathObj.ApiMappingId; + } } } } - 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 { + if (domain.apiType === Globals.apiTypes.rest) { + 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 basepath mapping."); + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Unable to create basepath mapping.\n`); + } + + } else if (domain.apiType === Globals.apiTypes.http + || domain.apiType === Globals.apiTypes.websocket) { // V2 HTTP and WEBSOCKET + + 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 basepath mapping."); + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: 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 { + if (domain.apiType === Globals.apiTypes.rest) { + const params = { + basePath: domain.currentBasePath, + 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 basepath mapping."); + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: Unable to update basepath mapping.\n`); + } + + } else if (domain.apiType === Globals.apiTypes.http + || domain.apiType === Globals.apiTypes.websocket) { // V2 HTTP and WEBSOCKET + + const params = { + ApiId: domain.apiId, + ApiMappingId: domain.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 basepath mapping."); + } catch (err) { + this.logIfDebug(err, domain.givenDomainName); + throw new Error(`Error: 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, }; @@ -564,48 +657,51 @@ 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, + basePath: domain.basePath, + domainName: domain.givenDomainName, }; // Make API call try { await this.apigateway.deleteBasePathMapping(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(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.DomainName = { - Value: domainInfo.domainName, + Value: domain.domainInfo.domainName, }; - if (domainInfo.hostedZoneId) { + if (domain.domainInfo.hostedZoneId) { service.provider.compiledCloudFormationTemplate.Outputs.HostedZoneId = { - Value: domainInfo.hostedZoneId, + Value: domain.domainInfo.hostedZoneId, }; } } @@ -614,26 +710,22 @@ 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 { + private printDomainSummary(domain: DomainConfig): void { this.serverless.cli.consoleLog(chalk.yellow.underline("Serverless 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}`); - } - 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 4aa7992f..c66bcd2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "serverless-domain-manager", - "version": "3.3.0", + "version": "3.3.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -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/package.json b/package.json index 102f8230..e7717b49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless-domain-manager", - "version": "3.3.1", + "version": "3.3.2", "engines": { "node": ">=4.0" }, diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts index 27bb5c0c..e046cd3d 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,10 +120,24 @@ 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); + }); + }); describe("Set Domain Name and Base Path", () => { - it("Creates basepath mapping", async () => { + it("Creates basepath mapping for REST api", async () => { AWS.mock("APIGateway", "createBasePathMapping", (params, callback) => { callback(null, params); }); @@ -129,11 +147,14 @@ describe("Custom Domain Plugin", () => { }); 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 +163,34 @@ describe("Custom Domain Plugin", () => { }); }); - it("Updates basepath mapping", async () => { + it("Creates basepath mapping for HTTP/Websocket api", async () => { + AWS.mock("ApiGatewayV2", "createApiMapping", (params, callback) => { + callback(null, params); + }); + const plugin = constructPlugin({ + apiType: "http", + basePath: "test_basepath", + domainName: "test_domain", + }); + 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 REST api", async () => { AWS.mock("APIGateway", "updateBasePathMapping", (params, callback) => { callback(null, params); }); @@ -151,12 +199,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.currentBasePath = "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 +222,72 @@ describe("Custom Domain Plugin", () => { }); }); + it("Updates basepath mapping for HTTP/Websocket api", async () => { + AWS.mock("ApiGatewayV2", "updateApiMapping", (params, callback) => { + callback(null, params); + }); + const plugin = constructPlugin({ + apiType: "http", + basePath: "test_basepath", + domainName: "test_domain", + }); + plugin.initializeVariables(); + + plugin.apigatewayV2 = new aws.ApiGatewayV2(); + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + dc.apiId = "test_api_id", + dc.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("APIGateway", "deleteBasePathMapping", (params, callback) => { + callback(null, params); + }); + + const plugin = constructPlugin({ + basePath: "test_basepath", + domainName: "test_domain", + }); + plugin.initializeVariables(); + + plugin.apigateway = new aws.APIGateway(); + + const spy = chai.spy.on(plugin.apigateway, "deleteBasePathMapping"); + + await plugin.removeBasePathMappings(); + expect(spy).to.have.been.called.with({ + basePath: "test_basepath", + domainName: "test_domain", + }); + }); + it("Add 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 +303,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 +330,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 +356,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 +383,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 +416,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 +429,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 +442,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"; - 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 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"}); + plugin.initializeVariables(); + plugin.apigatewayV2 = new aws.ApiGatewayV2(); + + 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 a domain name with specific TLS version", async () => { @@ -328,13 +480,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 +502,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 +558,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,7 +572,7 @@ describe("Custom Domain Plugin", () => { }); describe("Gets existing basepath mappings correctly", () => { - it("Returns undefined if no basepaths map to current restApiId", async () => { + it("Returns undefined if no basepaths map to current REST api", async () => { AWS.mock("APIGateway", "getBasePathMappings", (params, callback) => { callback(null, { items: [ @@ -423,15 +584,38 @@ 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); + + plugin.initializeVariables(); + + const result = await plugin.getBasePathMapping(dc); + expect(result).to.equal(undefined); + }); + + it("Returns undefined if no basepaths map to current HTTP api", async () => { + AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => { + callback(null, { + Items: [ + { ApiMappingKey: "(none)", ApiId: "test_rest_api_id_one", Stage: "test" }, + ], + }); + }); + + const plugin = constructPlugin({ + apiType: "http", + domainName: "test_domain", + }); + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + 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 () => { + it("Returns current REST api", async () => { AWS.mock("APIGateway", "getBasePathMappings", (params, callback) => { callback(null, { items: [ @@ -441,14 +625,43 @@ describe("Custom Domain Plugin", () => { }); const plugin = constructPlugin({ + apiType: Globals.apiTypes.rest, + basePath: "api", + domainName: "test_domain", + }); + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + + dc.apiId = "test_rest_api_id"; + + plugin.initializeVariables(); + + const result = await plugin.getBasePathMapping(dc); + expect(result).to.equal("api"); + }); + + it("Returns current HTTP api", async () => { + AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => { + callback(null, { + Items: [ + { ApiMappingId: "api", ApiId: "test_http_api_id", Stage: "test" }, + ], + }); + }); + + const plugin = constructPlugin({ + apiType: Globals.apiTypes.http, 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_http_api_id"; + plugin.initializeVariables(); - const result = await plugin.getBasePathMapping("test_rest_api_id"); + const result = await plugin.getBasePathMapping(dc); expect(result).to.equal("api"); }); @@ -458,8 +671,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 +686,80 @@ 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", + }); + 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", + }); + 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 +780,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 +803,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 +825,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: [ @@ -593,10 +882,12 @@ describe("Custom Domain Plugin", () => { domainName: "test_domain", }); plugin.apigateway = new aws.APIGateway(); - plugin.givenDomainName = plugin.serverless.service.custom.customDomain.domainName; + + const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain); + const spy = chai.spy.on(plugin.apigateway, "deleteDomainName"); - await plugin.deleteCustomDomain(); + await plugin.deleteCustomDomain(dc); expect(spy).to.be.called.with({ domainName: "test_domain", }); @@ -632,10 +923,9 @@ describe("Custom Domain Plugin", () => { plugin.initializeVariables(); plugin.apigateway = new aws.APIGateway(); 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(); }); @@ -657,9 +947,10 @@ 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 () => { @@ -681,10 +972,12 @@ 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 () => { @@ -706,9 +999,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 +1023,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 +1042,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 +1062,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 +1081,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 +1099,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 +1119,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 +1139,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 +1158,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 +1175,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 +1192,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 +1216,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 +1233,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 +1249,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 +1261,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"); @@ -999,14 +1291,14 @@ 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(); + + await plugin.domainSummaries(); - await plugin.domainSummary(); 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 +1316,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 +1330,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 +1344,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 +1355,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 +1363,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,