From 11d96c92d2e68c95543bf578d2855d824ca334ee Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 3 Oct 2024 12:03:00 -0400 Subject: [PATCH] ApiGatewayV2: share domain name --- .../components/aws/apigateway-websocket.ts | 102 +++++++++++------- platform/src/components/aws/apigatewayv2.ts | 94 +++++++++------- .../aws/helpers/apigatewayv2-domain.ts | 18 +++- 3 files changed, 137 insertions(+), 77 deletions(-) diff --git a/platform/src/components/aws/apigateway-websocket.ts b/platform/src/components/aws/apigateway-websocket.ts index 8bd615cff..e2c14f061 100644 --- a/platform/src/components/aws/apigateway-websocket.ts +++ b/platform/src/components/aws/apigateway-websocket.ts @@ -24,6 +24,7 @@ import { ApiGatewayWebSocketRoute } from "./apigateway-websocket-route"; import { setupApiGatewayAccount } from "./helpers/apigateway-account"; import { apigatewayv2, cloudwatch } from "@pulumi/aws"; import { permission } from "./permission"; +import { VisibleError } from "../error"; export interface ApiGatewayWebSocketArgs { /** @@ -206,7 +207,7 @@ export class ApiGatewayWebSocket extends Component implements Link.Linkable { private constructorOpts: ComponentResourceOptions; private api: apigatewayv2.Api; private stage: apigatewayv2.Stage; - private apigDomain?: apigatewayv2.DomainName; + private apigDomain?: Output; private apiMapping?: Output; private logGroup: cloudwatch.LogGroup; @@ -255,21 +256,28 @@ export class ApiGatewayWebSocket extends Component implements Link.Linkable { function normalizeDomain() { if (!args.domain) return; - // validate - output(args.domain).apply((domain) => { - if (typeof domain === "string") return; - - if (!domain.name) throw new Error(`Missing "name" for domain.`); - if (domain.dns === false && !domain.cert) - throw new Error(`No "cert" provided for domain with disabled DNS.`); - }); - - // normalize return output(args.domain).apply((domain) => { + // validate + if (typeof domain !== "string") { + if (domain.name && domain.nameId) + throw new VisibleError( + `Cannot configure both domain "name" and "nameId" for the "${name}" API.`, + ); + if (!domain.name && !domain.nameId) + throw new VisibleError( + `Either domain "name" or "nameId" is required for the "${name}" API.`, + ); + if (domain.dns === false && !domain.cert) + throw new VisibleError( + `Domain "cert" is required when "dns" is disabled for the "${name}" API.`, + ); + } + + // normalize const norm = typeof domain === "string" ? { name: domain } : domain; - return { name: norm.name, + nameId: norm.nameId, path: norm.path, dns: norm.dns === false ? undefined : norm.dns ?? awsDns(), cert: norm.cert, @@ -345,15 +353,16 @@ export class ApiGatewayWebSocket extends Component implements Link.Linkable { } function createSsl() { - if (!domain) return; + if (!domain) return output(undefined); return domain.apply((domain) => { if (domain.cert) return output(domain.cert); + if (domain.nameId) return output(undefined); return new DnsValidatedCertificate( `${name}Ssl`, { - domainName: domain.name, + domainName: domain.name!, dns: domain.dns!, }, { parent }, @@ -364,35 +373,43 @@ export class ApiGatewayWebSocket extends Component implements Link.Linkable { function createDomainName() { if (!domain || !certificateArn) return; - return new apigatewayv2.DomainName( - ...transform( - args.transform?.domainName, - `${name}DomainName`, - { - domainName: domain?.name, - domainNameConfiguration: { - certificateArn, - endpointType: "REGIONAL", - securityPolicy: "TLS_1_2", - }, - }, - { parent }, - ), - ); + return all([domain, certificateArn]).apply(([domain, certificateArn]) => { + return domain.nameId + ? apigatewayv2.DomainName.get( + `${name}DomainName`, + domain.nameId, + {}, + { parent }, + ) + : new apigatewayv2.DomainName( + ...transform( + args.transform?.domainName, + `${name}DomainName`, + { + domainName: domain.name!, + domainNameConfiguration: { + certificateArn: certificateArn!, + endpointType: "REGIONAL", + securityPolicy: "TLS_1_2", + }, + }, + { parent }, + ), + ); + }); } function createDnsRecords(): void { - if (!domain || !apigDomain) { - return; - } + if (!domain || !apigDomain) return; - domain.dns.apply((dns) => { - if (!dns) return; + domain.apply((domain) => { + if (!domain.dns) return; + if (domain.nameId) return; - dns.createAlias( + domain.dns.createAlias( name, { - name: domain.name, + name: domain.name!, aliasName: apigDomain.domainNameConfiguration.targetDomainName, aliasZone: apigDomain.domainNameConfiguration.hostedZoneId, }, @@ -463,11 +480,22 @@ export class ApiGatewayWebSocket extends Component implements Link.Linkable { * The underlying [resources](/docs/components/#nodes) this component creates. */ public get nodes() { + const self = this; return { /** * The Amazon API Gateway V2 API. */ api: this.api, + /** + * The API Gateway HTTP API domain name. + */ + get domainName() { + if (!self.apigDomain) + throw new VisibleError( + `"nodes.domainName" is not available when domain is not configured for the "${self.constructorName}" API.`, + ); + return self.apigDomain; + }, /** * The CloudWatch LogGroup for the access logs. */ @@ -531,7 +559,7 @@ export class ApiGatewayWebSocket extends Component implements Link.Linkable { */ public route( route: string, - handler: Input, + handler: Input, args: ApiGatewayWebSocketRouteArgs = {}, ) { const prefix = this.constructorName; diff --git a/platform/src/components/aws/apigatewayv2.ts b/platform/src/components/aws/apigatewayv2.ts index 832bcea55..d0e6838eb 100644 --- a/platform/src/components/aws/apigatewayv2.ts +++ b/platform/src/components/aws/apigatewayv2.ts @@ -634,7 +634,7 @@ export class ApiGatewayV2 extends Component implements Link.Linkable { private constructorArgs: ApiGatewayV2Args; private constructorOpts: ComponentResourceOptions; private api: apigatewayv2.Api; - private apigDomain?: apigatewayv2.DomainName; + private apigDomain?: Output; private apiMapping?: Output; private logGroup: cloudwatch.LogGroup; private vpcLink?: apigatewayv2.VpcLink; @@ -685,21 +685,28 @@ export class ApiGatewayV2 extends Component implements Link.Linkable { function normalizeDomain() { if (!args.domain) return; - // validate - output(args.domain).apply((domain) => { - if (typeof domain === "string") return; - - if (!domain.name) throw new Error(`Missing "name" for domain.`); - if (domain.dns === false && !domain.cert) - throw new Error(`No "cert" provided for domain with disabled DNS.`); - }); - - // normalize return output(args.domain).apply((domain) => { - const norm = typeof domain === "string" ? { name: domain } : domain; + // validate + if (typeof domain !== "string") { + if (domain.name && domain.nameId) + throw new VisibleError( + `Cannot configure both domain "name" and "nameId" for the "${name}" API.`, + ); + if (!domain.name && !domain.nameId) + throw new VisibleError( + `Either domain "name" or "nameId" is required for the "${name}" API.`, + ); + if (domain.dns === false && !domain.cert) + throw new VisibleError( + `Domain "cert" is required when "dns" is disabled for the "${name}" API.`, + ); + } + // normalize + const norm = typeof domain === "string" ? { name: domain } : domain; return { name: norm.name, + nameId: norm.nameId, path: norm.path, dns: norm.dns === false ? undefined : norm.dns ?? awsDns(), cert: norm.cert, @@ -810,15 +817,16 @@ export class ApiGatewayV2 extends Component implements Link.Linkable { } function createSsl() { - if (!domain) return; + if (!domain) return output(undefined); return domain.apply((domain) => { if (domain.cert) return output(domain.cert); + if (domain.nameId) return output(undefined); return new DnsValidatedCertificate( `${name}Ssl`, { - domainName: domain.name, + domainName: domain.name!, dns: domain.dns!, }, { parent }, @@ -829,35 +837,43 @@ export class ApiGatewayV2 extends Component implements Link.Linkable { function createDomainName() { if (!domain || !certificateArn) return; - return new apigatewayv2.DomainName( - ...transform( - args.transform?.domainName, - `${name}DomainName`, - { - domainName: domain?.name, - domainNameConfiguration: { - certificateArn, - endpointType: "REGIONAL", - securityPolicy: "TLS_1_2", - }, - }, - { parent }, - ), - ); + return all([domain, certificateArn]).apply(([domain, certificateArn]) => { + return domain.nameId + ? apigatewayv2.DomainName.get( + `${name}DomainName`, + domain.nameId, + {}, + { parent }, + ) + : new apigatewayv2.DomainName( + ...transform( + args.transform?.domainName, + `${name}DomainName`, + { + domainName: domain.name!, + domainNameConfiguration: { + certificateArn: certificateArn!, + endpointType: "REGIONAL", + securityPolicy: "TLS_1_2", + }, + }, + { parent }, + ), + ); + }); } function createDnsRecords(): void { - if (!domain || !apigDomain) { - return; - } + if (!domain || !apigDomain) return; - domain.dns.apply((dns) => { - if (!dns) return; + domain.apply((domain) => { + if (!domain.dns) return; + if (domain.nameId) return; - dns.createAlias( + domain.dns.createAlias( name, { - name: domain.name, + name: domain.name!, aliasName: apigDomain.domainNameConfiguration.targetDomainName, aliasZone: apigDomain.domainNameConfiguration.hostedZoneId, }, @@ -917,8 +933,8 @@ export class ApiGatewayV2 extends Component implements Link.Linkable { */ get domainName() { if (!self.apigDomain) - throw new Error( - `"nodes.domainName" is not available when a "domain" is configured.`, + throw new VisibleError( + `"nodes.domainName" is not available when domain is not configured for the "${self.constructorName}" API.`, ); return self.apigDomain; }, @@ -1150,7 +1166,7 @@ export class ApiGatewayV2 extends Component implements Link.Linkable { ) { if (!this.vpcLink) throw new VisibleError( - "To add private routes, you need to have a VPC link. Pass `vpc` to the `ApiGatewayV2` component to create a VPC link.", + `To add private routes, you need to have a VPC link. Configure "vpc" for the "${this.constructorName}" API to create a VPC link.`, ); const route = this.parseRoute(rawRoute); diff --git a/platform/src/components/aws/helpers/apigatewayv2-domain.ts b/platform/src/components/aws/helpers/apigatewayv2-domain.ts index 4d7c8b727..db2a0d868 100644 --- a/platform/src/components/aws/helpers/apigatewayv2-domain.ts +++ b/platform/src/components/aws/helpers/apigatewayv2-domain.ts @@ -2,6 +2,22 @@ import { Input } from "../../input"; import { Dns } from "../../dns"; export interface ApiGatewayV2DomainArgs { + /** + * Use an existing API Gateway domain name. + * + * By default, a new API Gateway domain name is created. If you'd like to use an existing + * domain name, set the `nameId` to the ID of the domain name and **do not** pass in `name`. + * + * @example + * ```js + * { + * domain: { + * nameId: "example.com" + * } + * } + * ``` + */ + nameId?: Input; /** * The custom domain you want to use. * @@ -24,7 +40,7 @@ export interface ApiGatewayV2DomainArgs { * } * ``` */ - name: Input; + name?: Input; /** * The base mapping for the custom domain. This adds a suffix to the URL of the API. *