diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 9c40aa83ae322..5b55d55f4c38a 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -941,6 +941,11 @@ const integration = new apigw.Integration({ }); ``` +The uri for the private integration, in the case of a VpcLink, will be set to the DNS name of +the VPC Link's NLB. If the VPC Link has multiple NLBs or the VPC Link is imported or the DNS +name cannot be determined for any other reason, the user is expected to specify the `uri` +property. + Any existing `VpcLink` resource can be imported into the CDK app via the `VpcLink.fromVpcLinkId()`. ```ts diff --git a/packages/@aws-cdk/aws-apigateway/lib/integration.ts b/packages/@aws-cdk/aws-apigateway/lib/integration.ts index 5b6a3040cb946..9d08a02b57bc2 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integration.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integration.ts @@ -1,6 +1,7 @@ import * as iam from '@aws-cdk/aws-iam'; +import { Lazy } from '@aws-cdk/core'; import { Method } from './method'; -import { IVpcLink } from './vpc-link'; +import { IVpcLink, VpcLink } from './vpc-link'; export interface IntegrationOptions { /** @@ -90,7 +91,7 @@ export interface IntegrationOptions { /** * The type of network connection to the integration endpoint. - * @default ConnectionType.Internet + * @default - ConnectionType.VPC_LINK if `vpcLink` property is configured; ConnectionType.Internet otherwise. */ readonly connectionType?: ConnectionType; @@ -199,10 +200,34 @@ export class Integration { * being integrated, access the REST API object, method ARNs, etc. */ public bind(_method: Method): IntegrationConfig { + let uri = this.props.uri; + const options = this.props.options; + + if (options?.connectionType === ConnectionType.VPC_LINK && uri === undefined) { + uri = Lazy.stringValue({ + // needs to be a lazy since the targets can be added to the VpcLink construct after initialization. + produce: () => { + const vpcLink = options.vpcLink; + if (vpcLink instanceof VpcLink) { + const targets = vpcLink._targetDnsNames; + if (targets.length > 1) { + throw new Error("'uri' is required when there are more than one NLBs in the VPC Link"); + } else { + return `http://${targets[0]}`; + } + } else { + throw new Error("'uri' is required when the 'connectionType' is VPC_LINK"); + } + }, + }); + } return { - options: this.props.options, + options: { + ...options, + connectionType: options?.vpcLink ? ConnectionType.VPC_LINK : options?.connectionType, + }, type: this.props.type, - uri: this.props.uri, + uri, integrationHttpMethod: this.props.integrationHttpMethod, }; } diff --git a/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts b/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts index 85bdf63d0022c..700d22c137cc1 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts @@ -61,7 +61,7 @@ export class VpcLink extends Resource implements IVpcLink { */ public readonly vpcLinkId: string; - private readonly targets = new Array(); + private readonly _targets = new Array(); constructor(scope: Construct, id: string, props: VpcLinkProps = {}) { super(scope, id, { @@ -83,17 +83,25 @@ export class VpcLink extends Resource implements IVpcLink { } public addTargets(...targets: elbv2.INetworkLoadBalancer[]) { - this.targets.push(...targets); + this._targets.push(...targets); + } + + /** + * Return the list of DNS names from the target NLBs. + * @internal + * */ + public get _targetDnsNames(): string[] { + return this._targets.map(t => t.loadBalancerDnsName); } protected validate(): string[] { - if (this.targets.length === 0) { + if (this._targets.length === 0) { return ['No targets added to vpc link']; } return []; } private renderTargets() { - return this.targets.map(nlb => nlb.loadBalancerArn); + return this._targets.map(nlb => nlb.loadBalancerArn); } } diff --git a/packages/@aws-cdk/aws-apigateway/test/test.integration.ts b/packages/@aws-cdk/aws-apigateway/test/test.integration.ts index 40047b53df745..e946ded961b29 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.integration.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.integration.ts @@ -1,3 +1,4 @@ +import { ABSENT, expect, haveResourceLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as iam from '@aws-cdk/aws-iam'; @@ -33,6 +34,89 @@ export = { test.done(); }, + 'uri is self determined from the NLB'(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const nlb = new elbv2.NetworkLoadBalancer(stack, 'NLB', { vpc }); + const link = new apigw.VpcLink(stack, 'link', { + targets: [nlb], + }); + const api = new apigw.RestApi(stack, 'restapi'); + const integration = new apigw.Integration({ + type: apigw.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'ANY', + options: { + connectionType: apigw.ConnectionType.VPC_LINK, + vpcLink: link, + }, + }); + api.root.addMethod('GET', integration); + + expect(stack).to(haveResourceLike('AWS::ApiGateway::Method', { + Integration: { + Uri: { + 'Fn::Join': [ + '', + [ + 'http://', + { + 'Fn::GetAtt': [ + 'NLB55158F82', + 'DNSName', + ], + }, + ], + ], + }, + }, + })); + + test.done(); + }, + + 'uri must be set for VpcLink with multiple NLBs'(test: Test) { + const app = new cdk.App(); + const stack = new cdk.Stack(app); + const vpc = new ec2.Vpc(stack, 'VPC'); + const nlb1 = new elbv2.NetworkLoadBalancer(stack, 'NLB1', { vpc }); + const nlb2 = new elbv2.NetworkLoadBalancer(stack, 'NLB2', { vpc }); + const link = new apigw.VpcLink(stack, 'link', { + targets: [nlb1, nlb2], + }); + const api = new apigw.RestApi(stack, 'restapi'); + const integration = new apigw.Integration({ + type: apigw.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'ANY', + options: { + connectionType: apigw.ConnectionType.VPC_LINK, + vpcLink: link, + }, + }); + api.root.addMethod('GET', integration); + test.throws(() => app.synth(), /'uri' is required when there are more than one NLBs in the VPC Link/); + + test.done(); + }, + + 'uri must be set when using an imported VpcLink'(test: Test) { + const app = new cdk.App(); + const stack = new cdk.Stack(app); + const link = apigw.VpcLink.fromVpcLinkId(stack, 'link', 'vpclinkid'); + const api = new apigw.RestApi(stack, 'restapi'); + const integration = new apigw.Integration({ + type: apigw.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'ANY', + options: { + connectionType: apigw.ConnectionType.VPC_LINK, + vpcLink: link, + }, + }); + api.root.addMethod('GET', integration); + test.throws(() => app.synth(), /'uri' is required when the 'connectionType' is VPC_LINK/); + + test.done(); + }, + 'connectionType of INTERNET and vpcLink are mutually exclusive'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -55,4 +139,60 @@ export = { }), /cannot set 'vpcLink' where 'connectionType' is INTERNET/); test.done(); }, + + 'connectionType is ABSENT when vpcLink is not specified'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigw.RestApi(stack, 'restapi'); + + // WHEN + const integration = new apigw.Integration({ + type: apigw.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'ANY', + }); + api.root.addMethod('ANY', integration); + + // THEN + expect(stack).to(haveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'ANY', + Integration: { + ConnectionType: ABSENT, + }, + })); + + test.done(); + }, + + 'connectionType defaults to VPC_LINK if vpcLink is configured'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const nlb = new elbv2.NetworkLoadBalancer(stack, 'NLB', { + vpc, + }); + const link = new apigw.VpcLink(stack, 'link', { + targets: [nlb], + }); + const api = new apigw.RestApi(stack, 'restapi'); + + // WHEN + const integration = new apigw.Integration({ + type: apigw.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'ANY', + options: { + vpcLink: link, + }, + }); + api.root.addMethod('ANY', integration); + + // THEN + expect(stack).to(haveResourceLike('AWS::ApiGateway::Method', { + HttpMethod: 'ANY', + Integration: { + ConnectionType: 'VPC_LINK', + }, + })); + + test.done(); + }, }; \ No newline at end of file