From 0f2a454979159a45e5f542c74a943d7e0fc2fd06 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Fri, 23 Oct 2020 12:31:41 -0600 Subject: [PATCH 01/51] feat: add load balancer lookups --- .../@aws-cdk/aws-ec2/lib/security-group.ts | 29 +- .../aws-elasticloadbalancingv2/README.md | 83 +++ .../lib/alb/application-listener.ts | 125 +++- .../lib/alb/application-load-balancer.ts | 63 +- .../lib/nlb/network-listener.ts | 43 +- .../lib/nlb/network-load-balancer.ts | 48 +- .../lib/shared/base-listener.ts | 85 ++- .../lib/shared/base-load-balancer.ts | 68 ++- .../aws-elasticloadbalancingv2/package.json | 4 + .../test/alb/listener.test.ts | 86 +++ .../test/alb/load-balancer.test.ts | 53 ++ .../test/nlb/listener.test.ts | 22 + .../test/nlb/load-balancer.test.ts | 60 ++ .../lib/cloud-assembly/context-queries.ts | 160 ++++- .../schema/cloud-assembly.schema.json | 7 +- .../schema/cloud-assembly.version.json | 2 +- .../cx-api/lib/context/load-balancer.ts | 69 +++ .../cx-api/lib/context/security-group.ts | 16 + packages/@aws-cdk/cx-api/lib/index.ts | 2 + packages/aws-cdk/lib/api/aws-auth/sdk.ts | 5 + .../aws-cdk/lib/context-providers/index.ts | 5 + .../lib/context-providers/load-balancers.ts | 292 +++++++++ .../lib/context-providers/security-groups.ts | 46 ++ .../context-providers/load-balancers.test.ts | 553 ++++++++++++++++++ packages/aws-cdk/test/util/mock-sdk.ts | 1 + 25 files changed, 1889 insertions(+), 38 deletions(-) create mode 100644 packages/@aws-cdk/cx-api/lib/context/load-balancer.ts create mode 100644 packages/@aws-cdk/cx-api/lib/context/security-group.ts create mode 100644 packages/aws-cdk/lib/context-providers/load-balancers.ts create mode 100644 packages/aws-cdk/lib/context-providers/security-groups.ts create mode 100644 packages/aws-cdk/test/context-providers/load-balancers.test.ts diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index 3681c61f686dc..ac90f5d7f9d5d 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -1,4 +1,6 @@ -import { Annotations, IResource, Lazy, Resource, ResourceProps, Stack, Token } from '@aws-cdk/core'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { Annotations, ContextProvider, IResource, Lazy, Resource, ResourceProps, Stack, Token } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { Connections } from './connections'; import { CfnSecurityGroup, CfnSecurityGroupEgress, CfnSecurityGroupIngress } from './ec2.generated'; @@ -294,6 +296,31 @@ export interface SecurityGroupImportOptions { * ``` */ export class SecurityGroup extends SecurityGroupBase { + /** + * Look up a security group by id. + */ + static fromLookup(scope: Construct, id: string, securityGroupId: string) { + const attributes: cxapi.SecurityGroupContextResponse = ContextProvider.getValue(scope, { + provider: cxschema.ContextProvider.SECURITY_GROUP_PROVIDER, + props: { + securityGroupId: securityGroupId, + } as cxschema.SecurityGroupContextQuery, + dummyValue: undefined, + }).value; + + // Default attributes for tests and the first pass. + const defaultAttributes: cxapi.SecurityGroupContextResponse = { + securityGroupId: securityGroupId, + allowAllOutbound: true, + }; + + const attributesToUse = attributes ?? defaultAttributes; + + return SecurityGroup.fromSecurityGroupId(scope, id, attributesToUse.securityGroupId, { + allowAllOutbound: attributesToUse.allowAllOutbound, + mutable: true, + }); + } /** * Import an existing security group into this app. diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index 93cbb9948b449..fb89dc80fcfdf 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -356,3 +356,86 @@ case for ECS Services for example), take a resource dependency on // has been associated with the LoadBalancer, before 'resource' is created. resourced.addDependency(targetGroup.loadBalancerDependency()); ``` + +## Looking up Load Balancers and Listeners + +You may look up load balancers and load balancer listeners by using one of the +following lookup methods: + +- `ApplicationLoadBalancer.fromlookup(options)` - Look up an application load + balancer. +- `ApplicationListener.fromLookup(options)` - Look up an application load + balancer listener. +- `NetworkLoadBalancer.fromLookup(options)` - Look up a network load balancer. +- `NetworkListener.fromLookup(options)` - Look up a network load balancer + listener. + +### Load Balancer lookup options + +You may look up a load balancer by ARN or by associated tags. When you look a +load balancer up by ARN, that load balancer will be returned unless CDK detects +that the load balancer is of the wrong type. When you look up a load balancer by +tags, CDK will return the first load balancer matching all specified tags. + +**Look up a Application Load Balancer by ARN** +```ts +const loadBalancer = ApplicationLoadBalancer.fromLookup(stack, 'ALB', { + loadBalancerArn: YOUR_ALB_ARN, +}); +``` + +**Look up an Application Load Balancer by tags** +```ts +const loadBalancer = ApplicationLoadBalancer.fromLookup(stack, 'ALB', { + loadBalancerTags: { + // Finds a load balancer matching all tags. + some: 'tag', + someother: 'tag', + }, +}); +``` + +## Load Balancer Listener lookup options + +You may look up a load balancer listener by the following criteria: + +- Associated load balancer arn +- Associated load balancer tags +- Listener port +- Listener protocol + +The lookup method will return the first matching listener rule. + +**Look up a Listener by associated Load Balancer, Port, and Protocol** + +```ts +const listener = ApplicationListener.fromLookup(stack, 'ALBListener', { + loadBalancerArn: YOUR_ALB_ARN, + listenerProtocol: ApplicationProtocol.HTTPS, + listenerPort: 443, +}); +``` + +**Look up a Listener by associated Load Balancer Tag, Port, and Protocol** + +```ts +const listener = ApplicationListener.fromLookup(stack, 'ALBListener', { + loadBalancerTags: { + Cluster: 'MyClusterName', + }, + listenerProtocol: ApplicationProtocol.HTTPS, + listenerPort: 443, +}); +``` + +**Look up a Network Listener by associated Load Balancer Tag, Port, and Protocol** + +```ts +const listener = NetworkListener.fromLookup(stack, 'ALBListener', { + loadBalancerTags: { + Cluster: 'MyClusterName', + }, + listenerProtocol: Protocol.TCP, + listenerPort: 12345, +}); +``` diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index d088008e7ce23..6e1759cc4251a 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -1,7 +1,9 @@ import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Duration, IResource, Lazy, Resource, Token } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; -import { BaseListener } from '../shared/base-listener'; +import { BaseListener, BaseListenerLookupUserOptions as BaseListenerLookupOptions } from '../shared/base-listener'; import { HealthCheck } from '../shared/base-target-group'; import { ApplicationProtocol, IpAddressType, SslPolicy } from '../shared/enums'; import { IListenerCertificate, ListenerCertificate } from '../shared/listener-certificate'; @@ -106,12 +108,38 @@ export interface ApplicationListenerProps extends BaseApplicationListenerProps { readonly loadBalancer: IApplicationLoadBalancer; } +/** + * Options for ApplicationListener lookup + */ +export interface ApplicationListenerLookupOptions extends BaseListenerLookupOptions { + /** + * Filter listeners by listener protocol + * @default - does not filter by listener protocol + */ + readonly listenerProtocol?: ApplicationProtocol; +} + /** * Define an ApplicationListener * * @resource AWS::ElasticLoadBalancingV2::Listener */ export class ApplicationListener extends BaseListener implements IApplicationListener { + /** + * Look up an ApplicationListener. + */ + static fromLookup(scope: Construct, id: string, options: ApplicationListenerLookupOptions): IApplicationListener { + const props = BaseListener._queryContextProvider(scope, { + userOptions: options, + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + listenerProtocol: options.listenerProtocol == ApplicationProtocol.HTTP + ? cxschema.LoadBalancerListenerProtocol.HTTP + : cxschema.LoadBalancerListenerProtocol.HTTPS, + }); + + return new LookedUpApplicationListener(scope, id, props); + } + /** * Import an existing listener */ @@ -517,7 +545,25 @@ export interface ApplicationListenerAttributes { readonly securityGroupAllowsAllOutbound?: boolean; } -class ImportedApplicationListener extends Resource implements IApplicationListener { +/** + * Props for `ImportedApplicationListenerBase` + */ +interface ImportedApplicationListenerBaseProps { + /** + * Arn of the listener. + */ + readonly listenerArn: string; + + /** + * Connections object. + */ + readonly connections: ec2.Connections; +} + +abstract class ImportedApplicationListenerBase extends Resource implements IApplicationListener { + /** + * Connections object. + */ public readonly connections: ec2.Connections; /** @@ -525,28 +571,20 @@ class ImportedApplicationListener extends Resource implements IApplicationListen */ public readonly listenerArn: string; - constructor(scope: Construct, id: string, props: ApplicationListenerAttributes) { + constructor(scope: Construct, id: string, props: ImportedApplicationListenerBaseProps) { super(scope, id); + this.connections = props.connections; this.listenerArn = props.listenerArn; + } - const defaultPort = props.defaultPort !== undefined ? ec2.Port.tcp(props.defaultPort) : undefined; - - let securityGroup: ec2.ISecurityGroup; - if (props.securityGroup) { - securityGroup = props.securityGroup; - } else if (props.securityGroupId) { - securityGroup = ec2.SecurityGroup.fromSecurityGroupId(scope, 'SecurityGroup', props.securityGroupId, { - allowAllOutbound: props.securityGroupAllowsAllOutbound, - }); - } else { - throw new Error('Either `securityGroup` or `securityGroupId` must be specified to import an application listener.'); - } - - this.connections = new ec2.Connections({ - securityGroups: [securityGroup], - defaultPort, - }); + /** + * Register that a connectable that has been added to this load balancer. + * + * Don't call this directly. It is called by ApplicationTargetGroup. + */ + public registerConnectable(connectable: ec2.IConnectable, portRange: ec2.Port): void { + this.connections.allowTo(connectable, portRange, 'Load balancer to target'); } /** @@ -599,14 +637,47 @@ class ImportedApplicationListener extends Resource implements IApplicationListen // eslint-disable-next-line max-len throw new Error('Can only call addTargets() when using a constructed ApplicationListener; construct a new TargetGroup and use addTargetGroup.'); } +} - /** - * Register that a connectable that has been added to this load balancer. - * - * Don't call this directly. It is called by ApplicationTargetGroup. - */ - public registerConnectable(connectable: ec2.IConnectable, portRange: ec2.Port): void { - this.connections.allowTo(connectable, portRange, 'Load balancer to target'); +/** + * An imported application listener. + */ +class ImportedApplicationListener extends ImportedApplicationListenerBase { + constructor(scope: Construct, id: string, props: ApplicationListenerAttributes) { + const defaultPort = props.defaultPort !== undefined ? ec2.Port.tcp(props.defaultPort) : undefined; + const connections = new ec2.Connections({ defaultPort }); + + super(scope, id, { + listenerArn: props.listenerArn, + connections, + }); + + let securityGroup: ec2.ISecurityGroup; + if (props.securityGroup) { + securityGroup = props.securityGroup; + } else if (props.securityGroupId) { + securityGroup = ec2.SecurityGroup.fromSecurityGroupId(scope, 'SecurityGroup', props.securityGroupId, { + allowAllOutbound: props.securityGroupAllowsAllOutbound, + }); + } else { + throw new Error('Either `securityGroup` or `securityGroupId` must be specified to import an application listener.'); + } + + this.connections.addSecurityGroup(securityGroup); + } +} + +class LookedUpApplicationListener extends ImportedApplicationListenerBase { + constructor(scope: Construct, id: string, props: cxapi.LoadBalancerListenerContextResponse) { + super(scope, id, { + listenerArn: props.listenerArn, + connections: new ec2.Connections({ defaultPort: ec2.Port.tcp(props.listenerPort) }), + }); + + for (const securityGroupId of props.securityGroupIds) { + const securityGroup = ec2.SecurityGroup.fromLookup(this, `SecurityGroup-${securityGroupId}`, securityGroupId); + this.connections.addSecurityGroup(securityGroup); + } } } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index 278cd8a5fbc44..8b159821a97ba 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -1,8 +1,10 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Duration, Lazy, Resource } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; -import { BaseLoadBalancer, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; +import { BaseLoadBalancer, BaseLoadBalancerLookupUserOptions as BaseLoadBalancerLookupOptions, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; import { IpAddressType, ApplicationProtocol } from '../shared/enums'; import { ApplicationListener, BaseApplicationListenerProps } from './application-listener'; import { ListenerAction } from './application-listener-action'; @@ -42,12 +44,30 @@ export interface ApplicationLoadBalancerProps extends BaseLoadBalancerProps { readonly idleTimeout?: Duration; } +/** + * Options for looking up an ApplicationLoadBalancer + */ +export interface ApplicationLoadBalancerLookupOptions extends BaseLoadBalancerLookupOptions { +} + /** * Define an Application Load Balancer * * @resource AWS::ElasticLoadBalancingV2::LoadBalancer */ export class ApplicationLoadBalancer extends BaseLoadBalancer implements IApplicationLoadBalancer { + /** + * Look up an application load balancer. + */ + public static fromLookup(scope: Construct, id: string, options: ApplicationLoadBalancerLookupOptions): IApplicationLoadBalancer { + const props = BaseLoadBalancer._queryContextProvider(scope, { + userOptions: options, + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + }); + + return new LookedUpApplicationLoadBalancer(scope, id, props); + } + /** * Import an existing Application Load Balancer */ @@ -569,7 +589,6 @@ class ImportedApplicationLoadBalancer extends Resource implements IApplicationLo constructor(scope: Construct, id: string, private readonly props: ApplicationLoadBalancerAttributes) { super(scope, id); - this.vpc = props.vpc; this.loadBalancerArn = props.loadBalancerArn; this.connections = new ec2.Connections({ securityGroups: [ec2.SecurityGroup.fromSecurityGroupId(this, 'SecurityGroup', props.securityGroupId, { @@ -598,6 +617,46 @@ class ImportedApplicationLoadBalancer extends Resource implements IApplicationLo } } +class LookedUpApplicationLoadBalancer extends Resource implements IApplicationLoadBalancer { + public readonly loadBalancerArn: string; + public readonly loadBalancerCanonicalHostedZoneId: string; + public readonly loadBalancerDnsName: string; + public readonly ipAddressType?: IpAddressType; + public readonly connections: ec2.Connections; + public readonly vpc?: ec2.IVpc; + + constructor(scope: Construct, id: string, props: cxapi.LoadBalancerContextResponse) { + super(scope, id); + + this.loadBalancerArn = props.loadBalancerArn; + this.loadBalancerCanonicalHostedZoneId = props.loadBalancerCanonicalHostedZoneId; + this.loadBalancerDnsName = props.loadBalancerDnsName; + + if (props.ipAddressType == cxapi.LoadBalancerIpAddressType.IPV4) { + this.ipAddressType = IpAddressType.IPV4; + } else if (props.ipAddressType == cxapi.LoadBalancerIpAddressType.DUAL_STACK) { + this.ipAddressType = IpAddressType.DUAL_STACK; + } + + this.vpc = ec2.Vpc.fromLookup(this, 'Vpc', { + vpcId: props.vpcId, + }); + + this.connections = new ec2.Connections(); + for (const securityGroupId of props.securityGroupIds) { + const securityGroup = ec2.SecurityGroup.fromLookup(this, `SecurityGroup-${securityGroupId}`, securityGroupId); + this.connections.addSecurityGroup(securityGroup); + } + } + + addListener(id: string, props: BaseApplicationListenerProps): ApplicationListener { + return new ApplicationListener(this, id, { + ...props, + loadBalancer: this, + }); + } +} + /** * Properties for a redirection config */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts index 17857a2ed4187..a7a5e55cba666 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts @@ -1,6 +1,7 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Duration, IResource, Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { BaseListener } from '../shared/base-listener'; +import { BaseListener, BaseListenerLookupUserOptions as BaseListenerLookupOptions } from '../shared/base-listener'; import { HealthCheck } from '../shared/base-target-group'; import { Protocol, SslPolicy } from '../shared/enums'; import { IListenerCertificate } from '../shared/listener-certificate'; @@ -86,12 +87,52 @@ export interface NetworkListenerProps extends BaseNetworkListenerProps { readonly loadBalancer: INetworkLoadBalancer; } +/** + * Options for looking up a network listener. + */ +export interface NetworkListenerLookupOptions extends BaseListenerLookupOptions { + /** + * Protocol of the listener port + * @default - listener is not filtered by protocol + */ + readonly listenerProtocol?: Protocol; +} + /** * Define a Network Listener * * @resource AWS::ElasticLoadBalancingV2::Listener */ export class NetworkListener extends BaseListener implements INetworkListener { + /** + * Looks up a network listener + */ + public static fromLookup(scope: Construct, id: string, options: NetworkListenerLookupOptions): INetworkListener { + let listenerProtocol: cxschema.LoadBalancerListenerProtocol | undefined = undefined; + if (options.listenerProtocol) { + validateNetworkProtocol(options.listenerProtocol); + + switch (options.listenerProtocol) { + case Protocol.TCP: listenerProtocol = cxschema.LoadBalancerListenerProtocol.TCP; break; + case Protocol.UDP: listenerProtocol = cxschema.LoadBalancerListenerProtocol.UDP; break; + case Protocol.TCP_UDP: listenerProtocol = cxschema.LoadBalancerListenerProtocol.TCP_UDP; break; + case Protocol.TLS: listenerProtocol = cxschema.LoadBalancerListenerProtocol.TLS; break; + } + } + + const props = BaseListener._queryContextProvider(scope, { + userOptions: options, + listenerProtocol: listenerProtocol, + loadBalancerType: cxschema.LoadBalancerType.NETWORK, + }); + + class LookedUp extends Resource implements INetworkListener { + public listenerArn = props.listenerArn; + } + + return new LookedUp(scope, id); + } + /** * Import an existing listener */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts index d8ba0a8b77506..908e49bebe9a7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts @@ -2,9 +2,11 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import { PolicyStatement, ServicePrincipal } from '@aws-cdk/aws-iam'; import { IBucket } from '@aws-cdk/aws-s3'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Resource } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; -import { BaseLoadBalancer, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; +import { BaseLoadBalancer, BaseLoadBalancerLookupUserOptions, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; import { BaseNetworkListenerProps, NetworkListener } from './network-listener'; /** @@ -51,12 +53,30 @@ export interface NetworkLoadBalancerAttributes { readonly vpc?: ec2.IVpc; } +/** + * Options for looking up an NetworkLoadBalancer + */ +export interface NetworkLoadBalancerLookupOptions extends BaseLoadBalancerLookupUserOptions { +} + /** * Define a new network load balancer * * @resource AWS::ElasticLoadBalancingV2::LoadBalancer */ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoadBalancer { + /** + * Looks up the network load balancer. + */ + public static fromLookup(scope: Construct, id: string, options: NetworkLoadBalancerLookupOptions): INetworkLoadBalancer { + const props = BaseLoadBalancer._queryContextProvider(scope, { + userOptions: options, + loadBalancerType: cxschema.LoadBalancerType.NETWORK, + }); + + return new LookedUpNetworkLoadBalancer(scope, id, props); + } + public static fromNetworkLoadBalancerAttributes(scope: Construct, id: string, attrs: NetworkLoadBalancerAttributes): INetworkLoadBalancer { class Import extends Resource implements INetworkLoadBalancer { public readonly loadBalancerArn = attrs.loadBalancerArn; @@ -289,3 +309,29 @@ export interface INetworkLoadBalancer extends ILoadBalancerV2, ec2.IVpcEndpointS */ addListener(id: string, props: BaseNetworkListenerProps): NetworkListener; } + +class LookedUpNetworkLoadBalancer extends Resource implements INetworkLoadBalancer { + public readonly loadBalancerCanonicalHostedZoneId: string; + public readonly loadBalancerDnsName: string; + public readonly loadBalancerArn: string; + public readonly vpc?: ec2.IVpc; + + constructor(scope: Construct, id: string, props: cxapi.LoadBalancerContextResponse) { + super(scope, id); + + this.loadBalancerArn = props.loadBalancerArn; + this.loadBalancerCanonicalHostedZoneId = props.loadBalancerCanonicalHostedZoneId; + this.loadBalancerDnsName = props.loadBalancerDnsName; + + this.vpc = ec2.Vpc.fromLookup(this, 'Vpc', { + vpcId: props.vpcId, + }); + } + + public addListener(lid: string, props: BaseNetworkListenerProps): NetworkListener { + return new NetworkListener(this, lid, { + loadBalancer: this, + ...props, + }); + } +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index 7866e522e0375..60945c3159b47 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -1,12 +1,95 @@ -import { Annotations, Lazy, Resource } from '@aws-cdk/core'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { Annotations, ContextProvider, Lazy, Resource, Token } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { CfnListener } from '../elasticloadbalancingv2.generated'; import { IListenerAction } from './listener-action'; +/** + * Options for ApplicationListener lookup + */ +export interface BaseListenerLookupUserOptions { + /** + * ARN of the application listener to look up + * @default - does not filter by listener arn + */ + readonly listenerArn?: string; + + /** + * Filter listeners by associated load balancer arn + * @default - does not filter by load balancer arn + */ + readonly loadBalancerArn?: string; + + /** + * Filter listeners by associated load balancer tags + * @default - does not filter by load balancer tags + */ + readonly loadBalancerTags?: Record; + + /** + * Filter listeners by listener port + * @default - does not filter by listener port + */ + readonly listenerPort?: number; +} + +/** + * Options for querying the load balancer listener context provider + * @internal + */ +export interface ListenerQueryContextProviderOptions { + /** + * User's provided options + */ + readonly userOptions: BaseListenerLookupUserOptions; + + /** + * Type of load balancer expected + */ + readonly loadBalancerType: cxschema.LoadBalancerType; + + /** + * Optional protocol of the listener to look up + */ + readonly listenerProtocol?: cxschema.LoadBalancerListenerProtocol; +} + /** * Base class for listeners */ export abstract class BaseListener extends Resource { + /** + * Queries the load balancer listener context provider for load balancer + * listener info. + * @internal + */ + protected static _queryContextProvider(scope: Construct, options: ListenerQueryContextProviderOptions) { + if (Token.isUnresolved(options.userOptions.loadBalancerArn) + ||Token.isUnresolved(options.userOptions.loadBalancerTags) + ||Token.isUnresolved(options.userOptions.listenerArn) + ||Token.isUnresolved(options.userOptions.listenerPort)) { + throw new Error('All arguments to look up a load balancer listener must be concrete (no Tokens)'); + } + + const props: cxapi.LoadBalancerListenerContextResponse = ContextProvider.getValue(scope, { + provider: cxschema.ContextProvider.LOAD_BALANCER_LISTENER_PROVIDER, + props: { + ...options.userOptions, + loadBalancerType: options.loadBalancerType, + listenerProtocol: options.listenerProtocol, + } as cxschema.LoadBalancerListenerContextQuery, + dummyValue: undefined, + }).value; + + const defaultProps: cxapi.LoadBalancerListenerContextResponse = { + listenerArn: `arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/${options.loadBalancerType}/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2`, + listenerPort: 80, + securityGroupIds: ['sg-123456789012'], + }; + + return props ?? defaultProps; + } /** * @attribute */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts index 1ca544187038a..591acc1d65eba 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts @@ -1,7 +1,9 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; -import { IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { ContextProvider, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { RegionInfo } from '@aws-cdk/region-info'; import { Construct } from 'constructs'; import { CfnLoadBalancer } from '../elasticloadbalancingv2.generated'; @@ -64,10 +66,74 @@ export interface ILoadBalancerV2 extends IResource { readonly loadBalancerDnsName: string; } +/** + * Options for looking up load balancers + */ +export interface BaseLoadBalancerLookupUserOptions { + /** + * Find by load balancer's ARN + * @default - does not search by load balancer arn + */ + readonly loadBalancerArn?: string; + + /** + * Match load balancer tags. + * @default - does not match load balancers by tags + */ + readonly loadBalancerTags?: Record; +} + +/** + * Options for query context provider + * @internal + */ +export interface LoadBalancerQueryContextProviderOptions { + /** + * User's lookup options + */ + readonly userOptions: BaseLoadBalancerLookupUserOptions; + + /** + * Type of load balancer + */ + readonly loadBalancerType: cxschema.LoadBalancerType; +} + /** * Base class for both Application and Network Load Balancers */ export abstract class BaseLoadBalancer extends Resource { + /** + * Queries the load balancer context provider for load balancer info. + * @internal + */ + protected static _queryContextProvider(scope: Construct, options: LoadBalancerQueryContextProviderOptions) { + if (Token.isUnresolved(options.userOptions.loadBalancerArn) + ||Token.isUnresolved(options.userOptions.loadBalancerTags)) { + throw new Error('All arguments to look up a load balancer must be concrete (no Tokens)'); + } + + const props: cxapi.LoadBalancerContextResponse = ContextProvider.getValue(scope, { + provider: cxschema.ContextProvider.LOAD_BALANCER_PROVIDER, + props: { + ...options.userOptions, + loadBalancerType: options.loadBalancerType, + } as cxschema.LoadBalancerContextQuery, + dummyValue: undefined, + }).value; + + const dummyProps: cxapi.LoadBalancerContextResponse = { + ipAddressType: cxapi.LoadBalancerIpAddressType.DUAL_STACK, + loadBalancerArn: `arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/${options.loadBalancerType}/my-load-balancer/50dc6c495c0c9188`, + loadBalancerCanonicalHostedZoneId: 'Z3DZXE0EXAMPLE', + loadBalancerDnsName: 'my-load-balancer-1234567890.us-west-2.elb.amazonaws.com', + securityGroupIds: ['sg-1234'], + vpcId: 'vpc-12345', + }; + + return props ?? dummyProps; + } + /** * The canonical hosted zone ID of this load balancer * diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json index 8e8bd7d3a8319..1fff4902df50a 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json @@ -85,7 +85,9 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", "constructs": "^3.0.4" }, @@ -97,7 +99,9 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^3.0.4", "@aws-cdk/region-info": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts index 18f162b1f8210..84f91967c8c6b 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -1364,6 +1364,92 @@ describe('tests', () => { }); }).toThrow(/Specify at most one/); }); + + describe('lookup', () => { + test('Can look up an ApplicationListener', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + // WHEN + const listener = elbv2.ApplicationListener.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // THEN + expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + expect(listener.listenerArn).toEqual('arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/application/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2'); + expect(listener.connections.securityGroups[0].securityGroupId).toEqual('sg-123456789012'); + }); + + test('Can add rules to a looked-up ApplicationListener', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + const listener = elbv2.ApplicationListener.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // WHEN + new elbv2.ApplicationListenerRule(stack, 'rule', { + listener, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['example.com']), + ], + action: elbv2.ListenerAction.fixedResponse(200), + priority: 5, + }); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 5, + }); + }); + + test('Can add certificates to a looked-up ApplicationListener', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + const listener = elbv2.ApplicationListener.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // WHEN + listener.addCertificateArns('certs', [ + 'arn:something', + ]); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Certificates: [ + { CertificateArn: 'arn:something' }, + ], + }); + }); + }); }); class ResourceWithLBDependency extends cdk.CfnResource { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts index 41063924e863c..e2745a2fb02aa 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts @@ -334,4 +334,57 @@ describe('tests', () => { Type: 'application', }); }); + + describe('lookup', () => { + test('Can look up an ApplicationLoadBalancer', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + // WHEN + const loadBalancer = elbv2.ApplicationLoadBalancer.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // THEN + expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::ApplicationLoadBalancer'); + expect(loadBalancer.loadBalancerArn).toEqual('arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188'); + expect(loadBalancer.loadBalancerCanonicalHostedZoneId).toEqual('Z3DZXE0EXAMPLE'); + expect(loadBalancer.loadBalancerDnsName).toEqual('my-load-balancer-1234567890.us-west-2.elb.amazonaws.com'); + expect(loadBalancer.ipAddressType).toEqual(elbv2.IpAddressType.DUAL_STACK); + expect(loadBalancer.connections.securityGroups[0].securityGroupId).toEqual('sg-1234'); + }); + + test('Can add listeners to a looked-up ApplicationLoadBalancer', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + const loadBalancer = elbv2.ApplicationLoadBalancer.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // WHEN + loadBalancer.addListener('listener', { + protocol: elbv2.ApplicationProtocol.HTTP, + }); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + }); + }); }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts index 4d3573c24d96e..d40197d00a280 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts @@ -388,6 +388,28 @@ describe('tests', () => { }); }).toThrow(/Specify at most one/); }); + + test('Can look up an NetworkListener', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + // WHEN + const listener = elbv2.NetworkListener.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // THEN + expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + expect(listener.listenerArn).toEqual('arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/network/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2'); + }); }); class ResourceWithLBDependency extends cdk.CfnResource { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts index ee6272f7abae9..88a3999ec0a6f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts @@ -401,4 +401,64 @@ describe('tests', () => { Type: 'network', }); }); + + describe('lookup', () => { + test('Can look up a NetworkLoadBalancer', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + // WHEN + const loadBalancer = elbv2.NetworkLoadBalancer.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // THEN + expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::NetworkLoadBalancer'); + expect(loadBalancer.loadBalancerArn).toEqual('arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/network/my-load-balancer/50dc6c495c0c9188'); + expect(loadBalancer.loadBalancerCanonicalHostedZoneId).toEqual('Z3DZXE0EXAMPLE'); + expect(loadBalancer.loadBalancerDnsName).toEqual('my-load-balancer-1234567890.us-west-2.elb.amazonaws.com'); + }); + + test('Can add listeners to a looked-up NetworkLoadBalancer', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + const loadBalancer = elbv2.NetworkLoadBalancer.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + const targetGroup = new elbv2.NetworkTargetGroup(stack, 'tg', { + vpc: loadBalancer.vpc, + port: 3000, + }); + + // WHEN + loadBalancer.addListener('listener', { + protocol: elbv2.Protocol.TCP_UDP, + port: 3000, + defaultAction: elbv2.NetworkListenerAction.forward([targetGroup]), + }); + + // THEN + expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::NetworkLoadBalancer'); + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + }); + }); }); + diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts index f2d11b76eac39..257877aaba80a 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts @@ -33,6 +33,20 @@ export enum ContextProvider { */ ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER = 'endpoint-service-availability-zones', + /** + * Application load balancer provider + */ + LOAD_BALANCER_PROVIDER = 'load-balancer', + + /** + * Application Listener provider + */ + LOAD_BALANCER_LISTENER_PROVIDER = 'load-balancer-listener', + + /** + * Security group provider + */ + SECURITY_GROUP_PROVIDER = 'security-group', } /** @@ -196,9 +210,153 @@ export interface EndpointServiceAvailabilityZonesContextQuery { readonly serviceName: string; } +/** + * Type of load balancer + */ +export enum LoadBalancerType { + /** + * Network load balancer + */ + NETWORK = 'network', + + /** + * Application load balancer + */ + APPLICATION = 'application', +} + +/** + * Filters for selecting load balancers + */ +export interface LoadBalancerFilter { + /** + * Filter load balancers by their type + * @default - does not filter by load balancer type + */ + readonly loadBalancerType?: LoadBalancerType; + + /** + * Find by load balancer's ARN + * @default - does not search by load balancer arn + */ + readonly loadBalancerArn?: string; + + /** + * Match load balancer tags + * @default - does not match load balancers by tags + */ + readonly loadBalancerTags?: Record; +} + +/** + * Query input for looking up an application load balancer + */ +export interface LoadBalancerContextQuery extends LoadBalancerFilter { + /** + * Query account + */ + readonly account: string; + + /** + * Query region + */ + readonly region: string; +} + +/** + * The protocol for connections from clients to the load balancer + */ +export enum LoadBalancerListenerProtocol { + /** + * HTTP protocol + */ + HTTP = 'HTTP', + + /** + * HTTPS protocol + */ + HTTPS = 'HTTPS', + + /** + * TCP protocol + */ + TCP = 'TCP', + + /** + * TLS protocol (offload?) + */ + TLS = 'TLS', + + /** + * UDP protocol + * */ + UDP = 'UDP', + + /** + * TCP and UDP protocol + * */ + TCP_UDP = 'TCP_UDP', +} + +/** + * Query input for looking up an application listener + */ +export interface LoadBalancerListenerContextQuery extends LoadBalancerFilter { + /** + * Query account + */ + readonly account: string; + + /** + * Query region + */ + readonly region: string; + + /** + * Find by listener's ARN + * @default - does not find by listener arn + */ + readonly listenerArn?: string; + + /** + * Filter listeners by listener protocol + * @default - does not filter by listener protocol + */ + readonly listenerProtocol?: LoadBalancerListenerProtocol; + + /** + * Filter listeners by listener port + * @default - does not filter by a listener port + */ + readonly listenerPort?: number; +} + +/** + * Query input for looking up a security group + */ +export interface SecurityGroupContextQuery { + /** + * Query account + */ + readonly account: string; + + /** + * Query region + */ + readonly region: string; + + /** + * Security group id + */ + readonly securityGroupId: string; +} + export type ContextQueryProperties = AmiContextQuery | AvailabilityZonesContextQuery | HostedZoneContextQuery | SSMParameterContextQuery | VpcContextQuery -| EndpointServiceAvailabilityZonesContextQuery; +| EndpointServiceAvailabilityZonesContextQuery +| LoadBalancerContextQuery +| LoadBalancerListenerContextQuery +| SecurityGroupContextQuery; diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index a154b78a0b508..ebdb7346d660a 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -409,7 +409,10 @@ "endpoint-service-availability-zones", "hosted-zone", "ssm", - "vpc-provider" + "vpc-provider", + "security-group", + "load-balancer", + "load-balancer-listener" ], "type": "string" }, @@ -598,4 +601,4 @@ } }, "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json index 2211f30276a5e..949e65028ffcf 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1 +1 @@ -{"version":"6.0.0"} +{"version":"6.1.0"} diff --git a/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts b/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts new file mode 100644 index 0000000000000..9c078b1dae49e --- /dev/null +++ b/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts @@ -0,0 +1,69 @@ +/** + * Load balancer ip address type. + */ +export enum LoadBalancerIpAddressType { + /** + * IPV4 ip address + */ + IPV4 = 'ipv4', + + /** + * Dual stack address + * */ + DUAL_STACK = 'dualstack', +} + +/** + * Properties of a discovered LoadBalancer + */ +export interface LoadBalancerContextResponse { + /** + * The ARN of the load balancer. + */ + readonly loadBalancerArn: string; + + /** + * The hosted zone ID of the load balancer's name. + */ + readonly loadBalancerCanonicalHostedZoneId: string; + + /** + * Load balancer's DNS name + */ + readonly loadBalancerDnsName: string; + + /** + * Type of IP address + */ + readonly ipAddressType: LoadBalancerIpAddressType; + + /** + * Load balancer's security groups + */ + readonly securityGroupIds: string[]; + + /** + * Load balancer's VPC + */ + readonly vpcId: string; +} + +/** + * Properties of a discovered ApplicationListener. + */ +export interface LoadBalancerListenerContextResponse { + /** + * The ARN of the listener. + */ + readonly listenerArn: string; + + /** + * The port the listener is listening on. + */ + readonly listenerPort: number; + + /** + * The security groups of the load balancer. + */ + readonly securityGroupIds: string[]; +} diff --git a/packages/@aws-cdk/cx-api/lib/context/security-group.ts b/packages/@aws-cdk/cx-api/lib/context/security-group.ts new file mode 100644 index 0000000000000..3cf59db485b34 --- /dev/null +++ b/packages/@aws-cdk/cx-api/lib/context/security-group.ts @@ -0,0 +1,16 @@ + +/** + * Properties of a discovered SecurityGroup. + */ +export interface SecurityGroupContextResponse { + /** + * The security group's id. + */ + readonly securityGroupId: string; + + /** + * Whether the security group of the load balancer allows all outbound + * traffic. + */ + readonly allowAllOutbound: boolean; +} diff --git a/packages/@aws-cdk/cx-api/lib/index.ts b/packages/@aws-cdk/cx-api/lib/index.ts index a6ac4977a6d17..8bab2151735c0 100644 --- a/packages/@aws-cdk/cx-api/lib/index.ts +++ b/packages/@aws-cdk/cx-api/lib/index.ts @@ -1,8 +1,10 @@ export * from './cxapi'; export * from './context/vpc'; export * from './context/ami'; +export * from './context/load-balancer'; export * from './context/availability-zones'; export * from './context/endpoint-service-availability-zones'; +export * from './context/security-group'; export * from './cloud-artifact'; export * from './artifacts/asset-manifest-artifact'; export * from './artifacts/cloudformation-artifact'; diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 2a20b9560b0fd..5825d0f627d39 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -29,6 +29,7 @@ export interface ISDK { s3(): AWS.S3; route53(): AWS.Route53; ecr(): AWS.ECR; + elbv2(): AWS.ELBv2; } /** @@ -92,6 +93,10 @@ export class SDK implements ISDK { return wrapServiceErrorHandling(new AWS.ECR(this.config)); } + public elbv2(): AWS.ELBv2 { + return wrapServiceErrorHandling(new AWS.ELBv2(this.config)); + } + public async currentAccount(): Promise { return cached(this, CURRENT_ACCOUNT_KEY, () => SDK.accountCache.fetch(this.credentials.accessKeyId, async () => { // if we don't have one, resolve from STS and store in cache. diff --git a/packages/aws-cdk/lib/context-providers/index.ts b/packages/aws-cdk/lib/context-providers/index.ts index a4a67351d4daf..e60ad6066d280 100644 --- a/packages/aws-cdk/lib/context-providers/index.ts +++ b/packages/aws-cdk/lib/context-providers/index.ts @@ -7,7 +7,9 @@ import { AmiContextProviderPlugin } from './ami'; import { AZContextProviderPlugin } from './availability-zones'; import { EndpointServiceAZContextProviderPlugin } from './endpoint-service-availability-zones'; import { HostedZoneContextProviderPlugin } from './hosted-zones'; +import { LoadBalancerListenerContextProviderPlugin, LoadBalancerContextProviderPlugin } from './load-balancers'; import { ContextProviderPlugin } from './provider'; +import { SecurityGroupContextProviderPlugin } from './security-groups'; import { SSMContextProviderPlugin } from './ssm-parameters'; import { VpcNetworkContextProviderPlugin } from './vpcs'; @@ -61,4 +63,7 @@ const availableContextProviders: ProviderMap = { [cxschema.ContextProvider.VPC_PROVIDER]: VpcNetworkContextProviderPlugin, [cxschema.ContextProvider.AMI_PROVIDER]: AmiContextProviderPlugin, [cxschema.ContextProvider.ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER]: EndpointServiceAZContextProviderPlugin, + [cxschema.ContextProvider.SECURITY_GROUP_PROVIDER]: SecurityGroupContextProviderPlugin, + [cxschema.ContextProvider.LOAD_BALANCER_PROVIDER]: LoadBalancerContextProviderPlugin, + [cxschema.ContextProvider.LOAD_BALANCER_LISTENER_PROVIDER]: LoadBalancerListenerContextProviderPlugin, }; diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts new file mode 100644 index 0000000000000..d26c0192e5134 --- /dev/null +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -0,0 +1,292 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as AWS from 'aws-sdk'; +import { Mode, SdkProvider } from '../api'; +import { ContextProviderPlugin } from './provider'; + +// Decreases line length +type LoadBalancerQuery = cxschema.LoadBalancerContextQuery; +type LoadBalancerResponse = cxapi.LoadBalancerContextResponse; + +/** + * Provides load balancer context information. + */ +export class LoadBalancerContextProviderPlugin implements ContextProviderPlugin { + constructor(private readonly aws: SdkProvider) { + } + + async getValue(query: LoadBalancerQuery): Promise { + const account: string = query.account!; + const region: string = query.region!; + + const elbv2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(account, region), Mode.ForReading)).elbv2(); + + const loadBalancers = await findLoadBalancers(elbv2, query); + + if (loadBalancers.length === 0) { + throw new Error(`No load balancers found matching ${JSON.stringify(query)}`); + } + + const loadBalancer = loadBalancers[0]; + + const ipAddressType = loadBalancer.IpAddressType == 'ipv4' + ? cxapi.LoadBalancerIpAddressType.IPV4 + : cxapi.LoadBalancerIpAddressType.DUAL_STACK; + + return { + loadBalancerArn: loadBalancer.LoadBalancerArn!, + loadBalancerCanonicalHostedZoneId: loadBalancer.CanonicalHostedZoneId!, + loadBalancerDnsName: loadBalancer.DNSName!, + vpcId: loadBalancer.VpcId!, + securityGroupIds: loadBalancer.SecurityGroups ?? [], + ipAddressType: ipAddressType, + }; + } +} + +// Decreases line length +type LoadBalancerListenerQuery = cxschema.LoadBalancerListenerContextQuery; +type LoadBalancerListenerResponse = cxapi.LoadBalancerListenerContextResponse; + +/** + * Provides load balancer listener context information + */ +export class LoadBalancerListenerContextProviderPlugin implements ContextProviderPlugin { + constructor(private readonly aws: SdkProvider) { + } + + async getValue(query: LoadBalancerListenerQuery): Promise { + const account: string = query.account!; + const region: string = query.region!; + + const elbv2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(account, region), Mode.ForReading)).elbv2(); + + if (query.listenerArn) { + // When we know a listener arn, we can query for that listener directly. + return this.getListenerDirectly(elbv2, query); + } else { + // When we don't know a listener arn, we need to find it by traversing + // all associated load balancers. + return this.getListenerByFilteringLoadBalancers(elbv2, query); + } + } + + /** + * Look up a listener by querying listeners for query's listener arn and then + * resolve its load balancer for the security group information. + */ + private async getListenerDirectly(elbv2: AWS.ELBv2, query: LoadBalancerListenerQuery) { + const listenerArn = query.listenerArn!; + const listenerResults = await elbv2.describeListeners({ ListenerArns: [listenerArn] }).promise(); + const listeners = (listenerResults.Listeners ?? []); + + if (listeners.length === 0) { + throw new Error(`No load balancer listeners found matching arn ${listenerArn}`); + } + + const listener = listeners[0]; + + const loadBalancers = await findLoadBalancers(elbv2, { + ...query, + loadBalancerArn: listener.LoadBalancerArn!, + }); + + if (loadBalancers.length === 0) { + throw new Error(`No associated load balancer found for listener arn ${listenerArn}`); + } + + const loadBalancer = loadBalancers[0]; + + return { + listenerArn: listener.ListenerArn!, + listenerPort: listener.Port!, + securityGroupIds: loadBalancer.SecurityGroups ?? [], + }; + } + + /** + * Look up a listener by starting from load balancers, filtering out + * unmatching load balancers, and then by querying the listeners of each load + * balancer and filtering out unmatching listeners. + */ + private async getListenerByFilteringLoadBalancers(elbv2: AWS.ELBv2, args: LoadBalancerListenerQuery) { + // Find matching load balancers + const loadBalancers = await findLoadBalancers(elbv2, args); + + if (loadBalancers.length === 0) { + throw new Error(`No associated load balancers found for load balancer listener query ${JSON.stringify(args)}`); + } + + // Find the first matching listener + return this.findFirstMatchingListener(elbv2, loadBalancers, args); + } + + /** + * Finds the first matching listener from the list of load balancers. + */ + private async findFirstMatchingListener(elbv2: AWS.ELBv2, loadBalancers: AWS.ELBv2.LoadBalancers, query: LoadBalancerListenerQuery) { + const loadBalancersByArn = indexLoadBalancersByArn(loadBalancers); + const loadBalancerArns = Object.keys(loadBalancersByArn); + + for await (const listener of describeListenersByLoadBalancerArn(elbv2, loadBalancerArns)) { + const loadBalancer = loadBalancersByArn[listener.LoadBalancerArn!]; + if (listenerMatchesQueryFilter(listener, query) && loadBalancer) { + return { + listenerArn: listener.ListenerArn!, + listenerPort: listener.Port!, + securityGroupIds: loadBalancer.SecurityGroups ?? [], + }; + } + } + + throw new Error(`No load balancer listeners found matching ${JSON.stringify(query)}`); + } +} + +/** + * Find load balancers by the given filter args. + */ +async function findLoadBalancers(elbv2: AWS.ELBv2, args: cxschema.LoadBalancerFilter) { + // List load balancers + let loadBalancers = await describeLoadBalancers(elbv2, { + LoadBalancerArns: args.loadBalancerArn ? [args.loadBalancerArn!] : undefined, + }); + + // Filter by load balancer type + if (args.loadBalancerType) { + loadBalancers = loadBalancers.filter(lb => lb.Type == args.loadBalancerType); + } + + // Filter by load balancer tags + if (args.loadBalancerTags) { + loadBalancers = await filterLoadBalancersByTags(elbv2, loadBalancers, args.loadBalancerTags); + } + + return loadBalancers; +} + +/** + * Helper to paginate over describeLoadBalancers + */ +async function describeLoadBalancers(elbv2: AWS.ELBv2, request: AWS.ELBv2.DescribeLoadBalancersInput) { + const loadBalancers = Array(); + let page: AWS.ELBv2.DescribeLoadBalancersOutput; + do { + page = await elbv2.describeLoadBalancers(request).promise(); + const pageItems = page.LoadBalancers ?? []; + for (const loadBalancer of pageItems) { + loadBalancers.push(loadBalancer); + } + } while (page.NextMarker); + + return loadBalancers; +} + +/** + * Describes the tags of each load balancer and returns the load balancers that + * match the given tags. + */ +async function filterLoadBalancersByTags(elbv2: AWS.ELBv2, loadBalancers: AWS.ELBv2.LoadBalancers, loadBalancerTags: Record) { + const loadBalancersByArn = indexLoadBalancersByArn(loadBalancers); + const loadBalancerArns = Object.keys(loadBalancersByArn); + const matchingLoadBalancers = Array(); + + // Consume the items of async generator. + for await (const tags of describeTags(elbv2, loadBalancerArns)) { + if (tagsMatch(tags, loadBalancerTags) && loadBalancersByArn[tags.ResourceArn!]) { + matchingLoadBalancers.push(loadBalancersByArn[tags.ResourceArn!]); + } + } + + return matchingLoadBalancers; +} + +/** + * Generator function that yields `TagDescriptions`. The API doesn't support + * pagination, so this generator breaks the resource list into chunks and issues + * the appropriate requests, yielding each tag description as it receives it. + */ +async function* describeTags(elbv2: AWS.ELBv2, resourceArns: string[]) { + // Max of 20 resource arns per request. + const chunkSize = 20; + for (let i = 0; i < resourceArns.length; i += chunkSize) { + const chunk = resourceArns.slice(i, Math.min(i + chunkSize, resourceArns.length)); + const chunkTags = await elbv2.describeTags({ + ResourceArns: chunk, + }).promise(); + + for (const tag of chunkTags.TagDescriptions ?? []) { + yield tag; + } + } +} + +/** + * Determines if the given TagDescription matches the required tags. + */ +function tagsMatch(tagDescription: AWS.ELBv2.TagDescription, requiredTags: Record) { + const tagsByName: Record = {}; + for (const tag of tagDescription.Tags ?? []) { + tagsByName[tag.Key!] = tag.Value!; + } + + for (const [requiredTag, requiredValue] of Object.entries(requiredTags)) { + if (tagsByName[requiredTag] !== requiredValue) { + return false; + } + } + + return true; +} + +/** + * Async generator that produces listener descriptions by traversing the + * pagination. Because describeListeners only lets you search by one load + * balancer arn at a time, we request them individually and yield the listeners + * as they come in. + */ +async function* describeListenersByLoadBalancerArn(elbv2: AWS.ELBv2, loadBalancerArns: string[]) { + for (const loadBalancerArn of loadBalancerArns) { + let page: AWS.ELBv2.DescribeListenersOutput | undefined; + do { + page = await elbv2.describeListeners({ + LoadBalancerArn: loadBalancerArn, + Marker: page ? page.NextMarker : undefined, + }).promise(); + + for (const listener of page.Listeners ?? []) { + yield listener; + } + } while (page.NextMarker); + } +} + +/** + * Determines if a listener matches the query filters. + */ +function listenerMatchesQueryFilter(listener: AWS.ELBv2.Listener, args: cxschema.LoadBalancerListenerContextQuery): boolean { + if (args.listenerPort && listener.Port != args.listenerPort) { + // No match. + return false; + } + + if (args.listenerProtocol && listener.Protocol != args.listenerProtocol) { + // No match. + return false; + } + + return true; +} + +/** + * Returns a record of load balancers indexed by their arns + */ +function indexLoadBalancersByArn(loadBalancers: AWS.ELBv2.LoadBalancer[]): Record { + const loadBalancersByArn: Record = {}; + + for (const loadBalancer of loadBalancers) { + loadBalancersByArn[loadBalancer.LoadBalancerArn!] = loadBalancer; + } + + return loadBalancersByArn; +} diff --git a/packages/aws-cdk/lib/context-providers/security-groups.ts b/packages/aws-cdk/lib/context-providers/security-groups.ts new file mode 100644 index 0000000000000..e4ab710f893b1 --- /dev/null +++ b/packages/aws-cdk/lib/context-providers/security-groups.ts @@ -0,0 +1,46 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as AWS from 'aws-sdk'; +import { Mode, SdkProvider } from '../api'; +import { ContextProviderPlugin } from './provider'; + +export class SecurityGroupContextProviderPlugin implements ContextProviderPlugin { + constructor(private readonly aws: SdkProvider) { + } + + async getValue(args: cxschema.SecurityGroupContextQuery): Promise { + const account: string = args.account!; + const region: string = args.region!; + + const ec2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(account, region), Mode.ForReading)).ec2(); + + const response = await ec2.describeSecurityGroups({ + GroupIds: [args.securityGroupId], + }).promise(); + + const securityGroups = response.SecurityGroups ?? []; + if (securityGroups.length === 0) { + throw new Error(`Could not find any SecurityGroups matching ${JSON.stringify(args)}`); + } + + const [securityGroup] = securityGroups; + + return { + securityGroupId: securityGroup.GroupId!, + allowAllOutbound: hasAllTrafficEgress(securityGroup), + }; + } +} + +function hasAllTrafficEgress(securityGroup: AWS.EC2.SecurityGroup) { + for (const ipPermission of securityGroup.IpPermissions ?? []) { + const hasAllTrafficCidr = Boolean(ipPermission.IpRanges?.find(m => m.CidrIp === '0.0.0.0/0')); + const isAllProtocols = ipPermission.IpProtocol == '-1'; + + if (hasAllTrafficCidr && isAllProtocols) { + return true; + } + } + + return false; +} diff --git a/packages/aws-cdk/test/context-providers/load-balancers.test.ts b/packages/aws-cdk/test/context-providers/load-balancers.test.ts new file mode 100644 index 0000000000000..bf91a12b7be0a --- /dev/null +++ b/packages/aws-cdk/test/context-providers/load-balancers.test.ts @@ -0,0 +1,553 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as aws from 'aws-sdk'; +import * as AWS from 'aws-sdk-mock'; +import { LoadBalancerListenerContextProviderPlugin, LoadBalancerContextProviderPlugin } from '../../lib/context-providers/load-balancers'; +import { MockSdkProvider } from '../util/mock-sdk'; + +AWS.setSDK(require.resolve('aws-sdk')); + +const mockSDK = new MockSdkProvider(); + +type AwsCallback = (err: Error | null, val: T) => void; + +afterEach(done => { + AWS.restore(); + done(); +}); + +describe('load balancer context provider plugin', () => { + test('errors when no matches are found', async () => { + // GIVEN + const provider = new LoadBalancerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [], + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerArn: 'arn:load-balancer1', + }), + ).rejects.toThrow(/No load balancers found/i); + }); + + test('looks up by arn', async () => { + // GIVEN + const provider = new LoadBalancerContextProviderPlugin(mockSDK); + + mockALBLookup({ + describeLoadBalancersExpected: { LoadBalancerArns: ['arn:load-balancer1'] }, + loadBalancers: [ + { + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer1', + DNSName: 'dns.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + }, + ], + }); + + // WHEN + const result = await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerArn: 'arn:load-balancer1', + }); + + // THEN + expect(result.ipAddressType).toEqual('ipv4'); + expect(result.loadBalancerArn).toEqual('arn:load-balancer1'); + expect(result.loadBalancerCanonicalHostedZoneId).toEqual('Z1234'); + expect(result.loadBalancerDnsName).toEqual('dns.example.com'); + expect(result.securityGroupIds).toEqual(['sg-1234']); + expect(result.vpcId).toEqual('vpc-1234'); + }); + + test('looks up by tags', async() => { + // GIVEN + const provider = new LoadBalancerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [ + { + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer1', + DNSName: 'dns1.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + }, + { + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer2', + DNSName: 'dns2.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + }, + ], + describeTagsExpected: { ResourceArns: ['arn:load-balancer1', 'arn:load-balancer2'] }, + tagDescriptions: [ + { + ResourceArn: 'arn:load-balancer1', + Tags: [ + { Key: 'some', Value: 'tag' }, + ], + }, + { + ResourceArn: 'arn:load-balancer2', + Tags: [ + { Key: 'some', Value: 'tag' }, + { Key: 'second', Value: 'tag2' }, + ], + }, + ], + }); + + // WHEN + await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerTags: { + some: 'tag', + second: 'tag2', + }, + }); + }); + + test('filters by type', async () => { + // GIVEN + const provider = new LoadBalancerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [ + { + IpAddressType: 'ipv4', + Type: 'network', + LoadBalancerArn: 'arn:load-balancer1', + DNSName: 'dns1.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + }, + { + IpAddressType: 'ipv4', + Type: 'application', + LoadBalancerArn: 'arn:load-balancer2', + DNSName: 'dns2.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + }, + ], + }); + + // WHEN + const loadBalancer = await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + }); + + expect(loadBalancer.loadBalancerArn).toEqual('arn:load-balancer2'); + }); +}); + +describe('load balancer listener context provider plugin', () => { + test('errors when no associated load balancers match', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [], + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + }), + ).rejects.toThrow(/No associated load balancers found/i); + }); + + test('errors when no listeners match', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [ + { + LoadBalancerArn: 'arn:load-balancer', + }, + ], + listeners: [ + { + ListenerArn: 'arn:listener', + Port: 80, + Protocol: 'HTTP', + }, + ], + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + listenerPort: 443, + listenerProtocol: cxschema.LoadBalancerListenerProtocol.HTTPS, + }), + ).rejects.toThrow(/No load balancer listeners found/i); + }); + + test('looks up by listener arn', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + describeListenersExpected: { ListenerArns: ['arn:listener-arn'] }, + listeners: [ + { + ListenerArn: 'arn:listener-arn', + LoadBalancerArn: 'arn:load-balancer-arn', + Port: 999, + }, + ], + describeLoadBalancersExpected: { LoadBalancerArns: ['arn:load-balancer-arn'] }, + loadBalancers: [ + { + LoadBalancerArn: 'arn:load-balancer-arn', + SecurityGroups: ['sg-1234', 'sg-2345'], + }, + ], + }); + + // WHEN + const listener = await provider.getValue({ + account: '1234', + region: 'us-east-1', + listenerArn: 'arn:listener-arn', + }); + + // THEN + expect(listener.listenerArn).toEqual('arn:listener-arn'); + expect(listener.listenerPort).toEqual(999); + expect(listener.securityGroupIds).toEqual(['sg-1234', 'sg-2345']); + }); + + test('looks up by associated load balancer arn', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + describeLoadBalancersExpected: { LoadBalancerArns: ['arn:load-balancer-arn1'] }, + loadBalancers: [ + { + LoadBalancerArn: 'arn:load-balancer-arn1', + SecurityGroups: ['sg-1234'], + }, + ], + + describeListenersExpected: { LoadBalancerArn: 'arn:load-balancer-arn1' }, + listeners: [ + { + // This one + ListenerArn: 'arn:listener-arn1', + LoadBalancerArn: 'arn:load-balancer-arn1', + Port: 80, + }, + ], + }); + + // WHEN + const listener = await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerArn: 'arn:load-balancer-arn1', + }); + + // THEN + expect(listener.listenerArn).toEqual('arn:listener-arn1'); + expect(listener.listenerPort).toEqual(80); + expect(listener.securityGroupIds).toEqual(['sg-1234']); + }); + + test('looks up by associated load balancer tags', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + describeLoadBalancersExpected: { LoadBalancerArns: undefined }, + loadBalancers: [ + { + // This one should have the wrong tags + LoadBalancerArn: 'arn:load-balancer-arn1', + SecurityGroups: ['sg-1234', 'sg-2345'], + }, + { + // Expecting this one + LoadBalancerArn: 'arn:load-balancer-arn2', + SecurityGroups: ['sg-3456', 'sg-4567'], + }, + ], + + describeTagsExpected: { ResourceArns: ['arn:load-balancer-arn1', 'arn:load-balancer-arn2'] }, + tagDescriptions: [ + { + ResourceArn: 'arn:load-balancer-arn1', + Tags: [], + }, + { + // Expecting this one + ResourceArn: 'arn:load-balancer-arn2', + Tags: [ + { Key: 'some', Value: 'tag' }, + ], + }, + ], + + describeListenersExpected: { LoadBalancerArn: 'arn:load-balancer-arn2' }, + listeners: [ + { + // This one + ListenerArn: 'arn:listener-arn1', + LoadBalancerArn: 'arn:load-balancer-arn2', + Port: 80, + }, + { + ListenerArn: 'arn:listener-arn2', + LoadBalancerArn: 'arn:load-balancer-arn2', + Port: 999, + }, + ], + }); + + // WHEN + const listener = await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerTags: { + some: 'tag', + }, + listenerPort: 999, + }); + + // THEN + expect(listener.listenerArn).toEqual('arn:listener-arn2'); + expect(listener.listenerPort).toEqual(999); + expect(listener.securityGroupIds).toEqual(['sg-3456', 'sg-4567']); + }); + + test('looks up by listener port and proto', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + AWS.mock('ELBv2', 'describeLoadBalancers', (_params: aws.ELBv2.DescribeLoadBalancersInput, cb: AwsCallback) => { + expect(_params).toEqual({}); + cb(null, { + LoadBalancers: [ + { + // Shouldn't have any matching listeners + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer1', + DNSName: 'dns1.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + }, + { + // Should have a matching listener + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer2', + DNSName: 'dns2.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-2345'], + VpcId: 'vpc-1234', + }, + ], + }); + }); + + AWS.mock('ELBv2', 'describeListeners', (params: aws.ELBv2.DescribeListenersInput, cb: AwsCallback) => { + if (params.LoadBalancerArn == 'arn:load-balancer1') { + cb(null, { + Listeners: [ + { + // Wrong port, wrong protocol => no match + ListenerArn: 'arn:listener-arn1', + LoadBalancerArn: 'arn:load-balancer1', + Protocol: 'HTTP', + Port: 80, + }, + { + // Wrong protocol, right port => no match + ListenerArn: 'arn:listener-arn3', + LoadBalancerArn: 'arn:load-balancer1', + Protocol: 'HTTPS', + Port: 443, + }, + { + // Wrong port, right protocol => no match + ListenerArn: 'arn:listener-arn4', + LoadBalancerArn: 'arn:load-balancer1', + Protocol: 'TCP', + Port: 999, + }, + ], + }); + } else if (params.LoadBalancerArn == 'arn:load-balancer2') { + cb(null, { + Listeners: [ + { + // Wrong port, wrong protocol => no match + ListenerArn: 'arn:listener-arn5', + LoadBalancerArn: 'arn:load-balancer2', + Protocol: 'HTTP', + Port: 80, + }, + { + // Right port, right protocol => match + ListenerArn: 'arn:listener-arn6', + LoadBalancerArn: 'arn:load-balancer2', + Port: 443, + Protocol: 'TCP', + }, + ], + }); + } else { + cb(new Error(`Unexpected request: ${JSON.stringify(params)}'`), {}); + } + }); + + // WHEN + const listener = await provider.getValue({ + account: '1234', + region: 'us-east-1', + listenerProtocol: cxschema.LoadBalancerListenerProtocol.TCP, + listenerPort: 443, + }); + + // THEN + expect(listener.listenerArn).toEqual('arn:listener-arn6'); + expect(listener.listenerPort).toEqual(443); + expect(listener.securityGroupIds).toEqual(['sg-2345']); + }); + + test('filters by associated load balancer type', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + describeLoadBalancersExpected: { LoadBalancerArns: undefined }, + loadBalancers: [ + { + // This one has wrong type => no match + LoadBalancerArn: 'arn:load-balancer-arn1', + SecurityGroups: [], + Type: 'application', + }, + { + // Right type => match + LoadBalancerArn: 'arn:load-balancer-arn2', + SecurityGroups: [], + Type: 'network', + }, + ], + + describeListenersExpected: { LoadBalancerArn: 'arn:load-balancer-arn2' }, + listeners: [ + { + ListenerArn: 'arn:listener-arn2', + LoadBalancerArn: 'arn:load-balancer-arn2', + Port: 443, + }, + ], + }); + + // WHEN + const listener = await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.NETWORK, + listenerPort: 443, + }); + + // THEN + expect(listener.listenerArn).toEqual('arn:listener-arn2'); + expect(listener.listenerPort).toEqual(443); + }); + + test('errors when associated load balancer is wrong type', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + describeListenersExpected: { ListenerArns: ['arn:listener-arn1'] }, + listeners: [ + { + ListenerArn: 'arn:listener-arn1', + LoadBalancerArn: 'arn:load-balancer-arn1', + Port: 443, + }, + ], + + describeLoadBalancersExpected: { LoadBalancerArns: ['arn:load-balancer-arn1'] }, + loadBalancers: [ + { + // This one has wrong type => no match + LoadBalancerArn: 'arn:load-balancer-arn1', + SecurityGroups: [], + Type: 'application', + }, + ], + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.NETWORK, + listenerArn: 'arn:listener-arn1', + }), + ).rejects.toThrow(/no associated load balancer found/i); + }); +}); + +interface ALBLookupOptions { + describeLoadBalancersExpected?: any; + loadBalancers?: aws.ELBv2.LoadBalancers; + describeTagsExpected?: any; + tagDescriptions?: aws.ELBv2.TagDescriptions; + describeListenersExpected?: any; + listeners?: aws.ELBv2.Listeners; +} + +function mockALBLookup(options: ALBLookupOptions) { + AWS.mock('ELBv2', 'describeLoadBalancers', (_params: aws.ELBv2.DescribeLoadBalancersInput, cb: AwsCallback) => { + if (options.describeLoadBalancersExpected !== undefined) { + expect(_params).toEqual(options.describeLoadBalancersExpected); + } + cb(null, { LoadBalancers: options.loadBalancers }); + }); + + AWS.mock('ELBv2', 'describeTags', (_params: aws.ELBv2.DescribeTagsInput, cb: AwsCallback) => { + if (options.describeTagsExpected !== undefined) { + expect(_params).toEqual(options.describeTagsExpected); + } + cb(null, { TagDescriptions: options.tagDescriptions }); + }); + + AWS.mock('ELBv2', 'describeListeners', (_params: aws.ELBv2.DescribeListenersInput, cb: AwsCallback) => { + if (options.describeListenersExpected !== undefined) { + expect(_params).toEqual(options.describeListenersExpected); + } + cb(null, { Listeners: options.listeners }); + }); +} diff --git a/packages/aws-cdk/test/util/mock-sdk.ts b/packages/aws-cdk/test/util/mock-sdk.ts index 2ce2bbd8cf346..c3ba69bb37111 100644 --- a/packages/aws-cdk/test/util/mock-sdk.ts +++ b/packages/aws-cdk/test/util/mock-sdk.ts @@ -87,6 +87,7 @@ export class MockSdk implements ISDK { public readonly s3 = jest.fn(); public readonly route53 = jest.fn(); public readonly ecr = jest.fn(); + public readonly elbv2 = jest.fn(); public currentAccount(): Promise { return Promise.resolve({ accountId: '123456789012', partition: 'aws' }); From 1215ffb230e439e420bef4275115aa09014c25a7 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sat, 24 Oct 2020 17:52:05 -0600 Subject: [PATCH 02/51] fix: schema version major version bump --- .../cloud-assembly-schema/schema/cloud-assembly.version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json index 949e65028ffcf..dead05af78863 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1 +1 @@ -{"version":"6.1.0"} +{"version":"7.0.0"} From 5efac1b4c7347da73736b096612132987d905ba5 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sat, 24 Oct 2020 18:00:37 -0600 Subject: [PATCH 03/51] fix: yarn update-schema was not run --- .../schema/cloud-assembly.schema.json | 137 +++++++++++++++++- .../schema/cloud-assembly.version.json | 2 +- 2 files changed, 131 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index ebdb7346d660a..8ea24df5ec8c5 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -247,11 +247,11 @@ "type": "object", "properties": { "key": { - "description": "Tag key.", + "description": "Tag key.\n\n(In the actual file on disk this will be cased as \"Key\", and the structure is\npatched to match this structure upon loading:\nhttps://github.com/aws/aws-cdk/blob/4aadaa779b48f35838cccd4e25107b2338f05547/packages/%40aws-cdk/cloud-assembly-schema/lib/manifest.ts#L137)", "type": "string" }, "value": { - "description": "Tag value.", + "description": "Tag value.\n\n(In the actual file on disk this will be cased as \"Value\", and the structure is\npatched to match this structure upon loading:\nhttps://github.com/aws/aws-cdk/blob/4aadaa779b48f35838cccd4e25107b2338f05547/packages/%40aws-cdk/cloud-assembly-schema/lib/manifest.ts#L137)", "type": "string" } }, @@ -391,6 +391,15 @@ }, { "$ref": "#/definitions/EndpointServiceAvailabilityZonesContextQuery" + }, + { + "$ref": "#/definitions/LoadBalancerContextQuery" + }, + { + "$ref": "#/definitions/LoadBalancerListenerContextQuery" + }, + { + "$ref": "#/definitions/SecurityGroupContextQuery" } ] } @@ -408,11 +417,11 @@ "availability-zones", "endpoint-service-availability-zones", "hosted-zone", - "ssm", - "vpc-provider", - "security-group", "load-balancer", - "load-balancer-listener" + "load-balancer-listener", + "security-group", + "ssm", + "vpc-provider" ], "type": "string" }, @@ -583,6 +592,120 @@ "serviceName" ] }, + "LoadBalancerContextQuery": { + "description": "Query input for looking up an application load balancer", + "type": "object", + "properties": { + "account": { + "description": "Query account", + "type": "string" + }, + "region": { + "description": "Query region", + "type": "string" + }, + "loadBalancerType": { + "description": "Filter load balancers by their type (Default - does not filter by load balancer type)", + "enum": [ + "application", + "network" + ], + "type": "string" + }, + "loadBalancerArn": { + "description": "Find by load balancer's ARN (Default - does not search by load balancer arn)", + "type": "string" + }, + "loadBalancerTags": { + "description": "Match load balancer tags (Default - does not match load balancers by tags)", + "$ref": "#/definitions/Record" + } + }, + "required": [ + "account", + "region" + ] + }, + "Record": { + "type": "object" + }, + "LoadBalancerListenerContextQuery": { + "description": "Query input for looking up an application listener", + "type": "object", + "properties": { + "account": { + "description": "Query account", + "type": "string" + }, + "region": { + "description": "Query region", + "type": "string" + }, + "listenerArn": { + "description": "Find by listener's ARN (Default - does not find by listener arn)", + "type": "string" + }, + "listenerProtocol": { + "description": "Filter listeners by listener protocol (Default - does not filter by listener protocol)", + "enum": [ + "HTTP", + "HTTPS", + "TCP", + "TCP_UDP", + "TLS", + "UDP" + ], + "type": "string" + }, + "listenerPort": { + "description": "Filter listeners by listener port (Default - does not filter by a listener port)", + "type": "number" + }, + "loadBalancerType": { + "description": "Filter load balancers by their type (Default - does not filter by load balancer type)", + "enum": [ + "application", + "network" + ], + "type": "string" + }, + "loadBalancerArn": { + "description": "Find by load balancer's ARN (Default - does not search by load balancer arn)", + "type": "string" + }, + "loadBalancerTags": { + "description": "Match load balancer tags (Default - does not match load balancers by tags)", + "$ref": "#/definitions/Record" + } + }, + "required": [ + "account", + "region" + ] + }, + "SecurityGroupContextQuery": { + "description": "Query input for looking up a security group", + "type": "object", + "properties": { + "account": { + "description": "Query account", + "type": "string" + }, + "region": { + "description": "Query region", + "type": "string" + }, + "securityGroupId": { + "description": "Security group id", + "type": "string" + } + }, + "required": [ + "account", + "region", + "securityGroupId" + ] + }, "RuntimeInfo": { "description": "Information about the application's runtime components.", "type": "object", @@ -601,4 +724,4 @@ } }, "$schema": "http://json-schema.org/draft-07/schema#" -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json index dead05af78863..bdc5a9f306dec 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1 +1 @@ -{"version":"7.0.0"} +{"version":"7.0.0"} \ No newline at end of file From 5c542f069330ed93e99c01c319ab367307dbd2ae Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sat, 24 Oct 2020 18:09:41 -0600 Subject: [PATCH 04/51] fix: artifacts from failed auto-refactor --- .../lib/alb/application-listener.ts | 2 +- .../aws-elasticloadbalancingv2/lib/nlb/network-listener.ts | 2 +- .../aws-elasticloadbalancingv2/lib/shared/base-listener.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 6e1759cc4251a..55e25910ac284 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -3,7 +3,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Duration, IResource, Lazy, Resource, Token } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; -import { BaseListener, BaseListenerLookupUserOptions as BaseListenerLookupOptions } from '../shared/base-listener'; +import { BaseListener, BaseListenerLookupOptions } from '../shared/base-listener'; import { HealthCheck } from '../shared/base-target-group'; import { ApplicationProtocol, IpAddressType, SslPolicy } from '../shared/enums'; import { IListenerCertificate, ListenerCertificate } from '../shared/listener-certificate'; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts index a7a5e55cba666..bb7c18919e487 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts @@ -1,7 +1,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Duration, IResource, Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { BaseListener, BaseListenerLookupUserOptions as BaseListenerLookupOptions } from '../shared/base-listener'; +import { BaseListener, BaseListenerLookupOptions } from '../shared/base-listener'; import { HealthCheck } from '../shared/base-target-group'; import { Protocol, SslPolicy } from '../shared/enums'; import { IListenerCertificate } from '../shared/listener-certificate'; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index 60945c3159b47..710542804fed2 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -8,7 +8,7 @@ import { IListenerAction } from './listener-action'; /** * Options for ApplicationListener lookup */ -export interface BaseListenerLookupUserOptions { +export interface BaseListenerLookupOptions { /** * ARN of the application listener to look up * @default - does not filter by listener arn @@ -42,7 +42,7 @@ export interface ListenerQueryContextProviderOptions { /** * User's provided options */ - readonly userOptions: BaseListenerLookupUserOptions; + readonly userOptions: BaseListenerLookupOptions; /** * Type of load balancer expected From e3c6e21b104601328a1e12e2497fdaec34e63af7 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sat, 24 Oct 2020 18:25:33 -0600 Subject: [PATCH 05/51] style: doc comment format --- packages/@aws-cdk/cx-api/lib/context/load-balancer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts b/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts index 9c078b1dae49e..f6ab16fd0cac7 100644 --- a/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts +++ b/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts @@ -9,7 +9,7 @@ export enum LoadBalancerIpAddressType { /** * Dual stack address - * */ + */ DUAL_STACK = 'dualstack', } From c0175d67b9c6fd840881fd7160d88b6d7c4bbc73 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sat, 24 Oct 2020 18:30:42 -0600 Subject: [PATCH 06/51] fix: accidental removal of vpc prop --- .../lib/alb/application-load-balancer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index 8b159821a97ba..ffecd13e3e98c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -589,6 +589,7 @@ class ImportedApplicationLoadBalancer extends Resource implements IApplicationLo constructor(scope: Construct, id: string, private readonly props: ApplicationLoadBalancerAttributes) { super(scope, id); + this.vpc = props.vpc; this.loadBalancerArn = props.loadBalancerArn; this.connections = new ec2.Connections({ securityGroups: [ec2.SecurityGroup.fromSecurityGroupId(this, 'SecurityGroup', props.securityGroupId, { From fe9d5c06a58067587819f2fe002c7b1b7a673109 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sat, 24 Oct 2020 18:43:28 -0600 Subject: [PATCH 07/51] refactor: change dummy security group variable name --- packages/@aws-cdk/aws-ec2/lib/security-group.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index ac90f5d7f9d5d..912c29118b58d 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -308,13 +308,13 @@ export class SecurityGroup extends SecurityGroupBase { dummyValue: undefined, }).value; - // Default attributes for tests and the first pass. - const defaultAttributes: cxapi.SecurityGroupContextResponse = { + // Dummy attributes for tests and the first pass. + const dummyAttributes: cxapi.SecurityGroupContextResponse = { securityGroupId: securityGroupId, allowAllOutbound: true, }; - const attributesToUse = attributes ?? defaultAttributes; + const attributesToUse = attributes ?? dummyAttributes; return SecurityGroup.fromSecurityGroupId(scope, id, attributesToUse.securityGroupId, { allowAllOutbound: attributesToUse.allowAllOutbound, From a1a0c74bfa01054e5b730b57f899ecfcbab3274a Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sat, 24 Oct 2020 18:51:37 -0600 Subject: [PATCH 08/51] chore: add test SecurityGroup.fromLookup() --- .../aws-ec2/test/security-group.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts index 918c8a79ceb89..e6e18832d99ad 100644 --- a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts @@ -1,5 +1,5 @@ import { expect, haveResource, not } from '@aws-cdk/assert'; -import { Intrinsic, Lazy, Stack, Token } from '@aws-cdk/core'; +import { App, Intrinsic, Lazy, Stack, Token } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; import { Peer, Port, SecurityGroup, Vpc } from '../lib'; @@ -293,4 +293,21 @@ nodeunitShim({ test.done(); }, }, + + 'can look up a security group'(test: Test) { + const app = new App(); + const stack = new Stack(app, 'stack', { + env: { + account: '1234', + region: 'us-east-1', + }, + }); + + const securityGroup = SecurityGroup.fromLookup(stack, 'stack', 'sg-1234'); + + test.equal(securityGroup.securityGroupId, 'sg-1234'); + test.equal(securityGroup.allowAllOutbound, true); + + test.done(); + }, }); From b372c874bc4ac4ac6489ef1521e8a79b5e5f67a0 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sat, 24 Oct 2020 19:02:59 -0600 Subject: [PATCH 09/51] chore: add tests for security group context provider --- .../lib/context-providers/security-groups.ts | 2 +- .../context-providers/security-groups.test.ts | 107 ++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 packages/aws-cdk/test/context-providers/security-groups.test.ts diff --git a/packages/aws-cdk/lib/context-providers/security-groups.ts b/packages/aws-cdk/lib/context-providers/security-groups.ts index e4ab710f893b1..4f4b65195ae47 100644 --- a/packages/aws-cdk/lib/context-providers/security-groups.ts +++ b/packages/aws-cdk/lib/context-providers/security-groups.ts @@ -20,7 +20,7 @@ export class SecurityGroupContextProviderPlugin implements ContextProviderPlugin const securityGroups = response.SecurityGroups ?? []; if (securityGroups.length === 0) { - throw new Error(`Could not find any SecurityGroups matching ${JSON.stringify(args)}`); + throw new Error(`No security groups found matching ${JSON.stringify(args)}`); } const [securityGroup] = securityGroups; diff --git a/packages/aws-cdk/test/context-providers/security-groups.test.ts b/packages/aws-cdk/test/context-providers/security-groups.test.ts new file mode 100644 index 0000000000000..13a68a7f710f1 --- /dev/null +++ b/packages/aws-cdk/test/context-providers/security-groups.test.ts @@ -0,0 +1,107 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as aws from 'aws-sdk'; +import * as AWS from 'aws-sdk-mock'; +import { LoadBalancerListenerContextProviderPlugin, LoadBalancerContextProviderPlugin } from '../../lib/context-providers/load-balancers'; +import { SecurityGroupContextProviderPlugin } from '../../lib/context-providers/security-groups'; +import { MockSdkProvider } from '../util/mock-sdk'; + +AWS.setSDK(require.resolve('aws-sdk')); + +const mockSDK = new MockSdkProvider(); + +type AwsCallback = (err: Error | null, val: T) => void; + +afterEach(done => { + AWS.restore(); + done(); +}); + +describe('security group context provider plugin', () => { + test('errors when no matches are found', async () => { + // GIVEN + const provider = new SecurityGroupContextProviderPlugin(mockSDK); + + AWS.mock('EC2', 'describeSecurityGroups', (_params: aws.EC2.DescribeSecurityGroupsRequest, cb: AwsCallback) => { + cb(null, { SecurityGroups: [] }); + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + securityGroupId: 'sg-1234', + }), + ).rejects.toThrow(/No security groups found/i); + }); + + test('looks up by security group id', async () => { + // GIVEN + const provider = new SecurityGroupContextProviderPlugin(mockSDK); + + AWS.mock('EC2', 'describeSecurityGroups', (_params: aws.EC2.DescribeSecurityGroupsRequest, cb: AwsCallback) => { + expect(_params).toEqual({ GroupIds: ['sg-1234'] }); + cb(null, { + SecurityGroups: [ + { + GroupId: 'sg-1234', + IpPermissionsEgress: [ + { + IpProtocol: '-1', + IpRanges: [ + { CidrIp: '0.0.0.0/0' }, + ], + }, + ], + }, + ], + }); + }); + + // WHEN + const res = await provider.getValue({ + account: '1234', + region: 'us-east-1', + securityGroupId: 'sg-1234', + }); + + // THEN + expect(res.securityGroupId).toEqual('sg-1234'); + expect(res.allowAllOutbound).toEqual(true); + }); + + test('detects non all-outbound egress', async () => { + // GIVEN + const provider = new SecurityGroupContextProviderPlugin(mockSDK); + + AWS.mock('EC2', 'describeSecurityGroups', (_params: aws.EC2.DescribeSecurityGroupsRequest, cb: AwsCallback) => { + expect(_params).toEqual({ GroupIds: ['sg-1234'] }); + cb(null, { + SecurityGroups: [ + { + GroupId: 'sg-1234', + IpPermissionsEgress: [ + { + IpProtocol: '-1', + IpRanges: [ + { CidrIp: '10.0.0.0/16' }, + ], + }, + ], + }, + ], + }); + }); + + // WHEN + const res = await provider.getValue({ + account: '1234', + region: 'us-east-1', + securityGroupId: 'sg-1234', + }); + + // THEN + expect(res.securityGroupId).toEqual('sg-1234'); + expect(res.allowAllOutbound).toEqual(false); + }); +}); From aef71d6655158cb4da230e8a46034762d2f98c3e Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sat, 24 Oct 2020 20:03:42 -0600 Subject: [PATCH 10/51] fix: imports not used --- packages/aws-cdk/test/context-providers/security-groups.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/aws-cdk/test/context-providers/security-groups.test.ts b/packages/aws-cdk/test/context-providers/security-groups.test.ts index 13a68a7f710f1..032e90e5960ff 100644 --- a/packages/aws-cdk/test/context-providers/security-groups.test.ts +++ b/packages/aws-cdk/test/context-providers/security-groups.test.ts @@ -1,7 +1,5 @@ -import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as aws from 'aws-sdk'; import * as AWS from 'aws-sdk-mock'; -import { LoadBalancerListenerContextProviderPlugin, LoadBalancerContextProviderPlugin } from '../../lib/context-providers/load-balancers'; import { SecurityGroupContextProviderPlugin } from '../../lib/context-providers/security-groups'; import { MockSdkProvider } from '../util/mock-sdk'; From d2ab397427472de09b43b46feb60c698a60e33f9 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sat, 24 Oct 2020 20:28:20 -0600 Subject: [PATCH 11/51] fix: does not identify all traffic egress --- .../lib/context-providers/security-groups.ts | 7 ++- .../context-providers/security-groups.test.ts | 45 ++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk/lib/context-providers/security-groups.ts b/packages/aws-cdk/lib/context-providers/security-groups.ts index 4f4b65195ae47..d1cda6d070626 100644 --- a/packages/aws-cdk/lib/context-providers/security-groups.ts +++ b/packages/aws-cdk/lib/context-providers/security-groups.ts @@ -32,8 +32,11 @@ export class SecurityGroupContextProviderPlugin implements ContextProviderPlugin } } -function hasAllTrafficEgress(securityGroup: AWS.EC2.SecurityGroup) { - for (const ipPermission of securityGroup.IpPermissions ?? []) { +/** + * @internal + */ +export function hasAllTrafficEgress(securityGroup: AWS.EC2.SecurityGroup) { + for (const ipPermission of securityGroup.IpPermissionsEgress ?? []) { const hasAllTrafficCidr = Boolean(ipPermission.IpRanges?.find(m => m.CidrIp === '0.0.0.0/0')); const isAllProtocols = ipPermission.IpProtocol == '-1'; diff --git a/packages/aws-cdk/test/context-providers/security-groups.test.ts b/packages/aws-cdk/test/context-providers/security-groups.test.ts index 032e90e5960ff..1f391ba50c8df 100644 --- a/packages/aws-cdk/test/context-providers/security-groups.test.ts +++ b/packages/aws-cdk/test/context-providers/security-groups.test.ts @@ -1,6 +1,6 @@ import * as aws from 'aws-sdk'; import * as AWS from 'aws-sdk-mock'; -import { SecurityGroupContextProviderPlugin } from '../../lib/context-providers/security-groups'; +import { hasAllTrafficEgress, SecurityGroupContextProviderPlugin } from '../../lib/context-providers/security-groups'; import { MockSdkProvider } from '../util/mock-sdk'; AWS.setSDK(require.resolve('aws-sdk')); @@ -102,4 +102,47 @@ describe('security group context provider plugin', () => { expect(res.securityGroupId).toEqual('sg-1234'); expect(res.allowAllOutbound).toEqual(false); }); + + test('identifies allTrafficEgress from SecurityGroup permissions', () => { + expect( + hasAllTrafficEgress({ + IpPermissionsEgress: [ + { + IpProtocol: '-1', + IpRanges: [ + { CidrIp: '0.0.0.0/0' }, + ], + }, + ], + }), + ).toBe(true); + }); + + test('identifies lacking allTrafficEgress from SecurityGroup permissions', () => { + expect( + hasAllTrafficEgress({ + IpPermissionsEgress: [ + { + IpProtocol: '-1', + IpRanges: [ + { CidrIp: '10.0.0.0/16' }, + ], + }, + ], + }), + ).toBe(false); + + expect( + hasAllTrafficEgress({ + IpPermissions: [ + { + IpProtocol: 'TCP', + IpRanges: [ + { CidrIp: '0.0.0.0/0' }, + ], + }, + ], + }), + ).toBe(false); + }); }); From 9c1ae33d6982e39e047774b412ca2ca9de893f09 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sat, 24 Oct 2020 22:53:54 -0600 Subject: [PATCH 12/51] chore: add loadAssemblyManifest to allowed breaking changes --- allowed-breaking-changes.txt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index 9217e4ca2e773..15dc753350874 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -45,4 +45,12 @@ incompatible-argument:@aws-cdk/aws-ecs.Ec2TaskDefinition.addVolume incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition. incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition.addVolume incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition. -incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition.addVolume \ No newline at end of file +incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition.addVolume + +# The new ELBv2 context queries added these arms to the ContextQueryProperties +# union: +# | LoadBalancerContextQuery +# | LoadBalancerListenerContextQuery +# | SecurityGroupContextQuery +# These additions changed the signature of loadAssemblyManifest() +change-return-type:@aws-cdk/cloud-assembly-schema.Manifest.loadAssemblyManifest From cf4da2d740d3643aee5c686dbd26518a1e6bfe8c Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sat, 24 Oct 2020 23:52:18 -0600 Subject: [PATCH 13/51] fix: artifacts from other failed auto-refactor --- .../lib/alb/application-load-balancer.ts | 2 +- .../lib/nlb/network-load-balancer.ts | 4 ++-- .../lib/shared/base-load-balancer.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index ffecd13e3e98c..73266c7577e65 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -4,7 +4,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Duration, Lazy, Resource } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; -import { BaseLoadBalancer, BaseLoadBalancerLookupUserOptions as BaseLoadBalancerLookupOptions, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; +import { BaseLoadBalancer, BaseLoadBalancerLookupOptions, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; import { IpAddressType, ApplicationProtocol } from '../shared/enums'; import { ApplicationListener, BaseApplicationListenerProps } from './application-listener'; import { ListenerAction } from './application-listener-action'; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts index 908e49bebe9a7..74dd1f060509f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts @@ -6,7 +6,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Resource } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; -import { BaseLoadBalancer, BaseLoadBalancerLookupUserOptions, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; +import { BaseLoadBalancer, BaseLoadBalancerLookupOptions, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; import { BaseNetworkListenerProps, NetworkListener } from './network-listener'; /** @@ -56,7 +56,7 @@ export interface NetworkLoadBalancerAttributes { /** * Options for looking up an NetworkLoadBalancer */ -export interface NetworkLoadBalancerLookupOptions extends BaseLoadBalancerLookupUserOptions { +export interface NetworkLoadBalancerLookupOptions extends BaseLoadBalancerLookupOptions { } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts index 591acc1d65eba..0cbeba29510d3 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts @@ -69,7 +69,7 @@ export interface ILoadBalancerV2 extends IResource { /** * Options for looking up load balancers */ -export interface BaseLoadBalancerLookupUserOptions { +export interface BaseLoadBalancerLookupOptions { /** * Find by load balancer's ARN * @default - does not search by load balancer arn @@ -91,7 +91,7 @@ export interface LoadBalancerQueryContextProviderOptions { /** * User's lookup options */ - readonly userOptions: BaseLoadBalancerLookupUserOptions; + readonly userOptions: BaseLoadBalancerLookupOptions; /** * Type of load balancer From 5e5ffeff45a90fe9dbdd33c208deec672e4ec121 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Sun, 25 Oct 2020 00:09:03 -0600 Subject: [PATCH 14/51] fix: typos in doc comments --- .../lib/cloud-assembly/context-queries.ts | 14 +++++++------- .../schema/cloud-assembly.schema.json | 8 ++++---- .../@aws-cdk/cx-api/lib/context/load-balancer.ts | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts index 257877aaba80a..37de5860f4e85 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts @@ -34,12 +34,12 @@ export enum ContextProvider { ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER = 'endpoint-service-availability-zones', /** - * Application load balancer provider + * Load balancer provider */ LOAD_BALANCER_PROVIDER = 'load-balancer', /** - * Application Listener provider + * Load balancer listener provider */ LOAD_BALANCER_LISTENER_PROVIDER = 'load-balancer-listener', @@ -249,7 +249,7 @@ export interface LoadBalancerFilter { } /** - * Query input for looking up an application load balancer + * Query input for looking up a load balancer */ export interface LoadBalancerContextQuery extends LoadBalancerFilter { /** @@ -283,7 +283,7 @@ export enum LoadBalancerListenerProtocol { TCP = 'TCP', /** - * TLS protocol (offload?) + * TLS protocol */ TLS = 'TLS', @@ -299,7 +299,7 @@ export enum LoadBalancerListenerProtocol { } /** - * Query input for looking up an application listener + * Query input for looking up a load balancer listener */ export interface LoadBalancerListenerContextQuery extends LoadBalancerFilter { /** @@ -313,13 +313,13 @@ export interface LoadBalancerListenerContextQuery extends LoadBalancerFilter { readonly region: string; /** - * Find by listener's ARN + * Find by listener's arn * @default - does not find by listener arn */ readonly listenerArn?: string; /** - * Filter listeners by listener protocol + * Filter by listener protocol * @default - does not filter by listener protocol */ readonly listenerProtocol?: LoadBalancerListenerProtocol; diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index 8ea24df5ec8c5..398de4f0eeffa 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -593,7 +593,7 @@ ] }, "LoadBalancerContextQuery": { - "description": "Query input for looking up an application load balancer", + "description": "Query input for looking up a load balancer", "type": "object", "properties": { "account": { @@ -630,7 +630,7 @@ "type": "object" }, "LoadBalancerListenerContextQuery": { - "description": "Query input for looking up an application listener", + "description": "Query input for looking up a load balancer listener", "type": "object", "properties": { "account": { @@ -642,11 +642,11 @@ "type": "string" }, "listenerArn": { - "description": "Find by listener's ARN (Default - does not find by listener arn)", + "description": "Find by listener's arn (Default - does not find by listener arn)", "type": "string" }, "listenerProtocol": { - "description": "Filter listeners by listener protocol (Default - does not filter by listener protocol)", + "description": "Filter by listener protocol (Default - does not filter by listener protocol)", "enum": [ "HTTP", "HTTPS", diff --git a/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts b/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts index f6ab16fd0cac7..bea7c432de57f 100644 --- a/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts +++ b/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts @@ -14,7 +14,7 @@ export enum LoadBalancerIpAddressType { } /** - * Properties of a discovered LoadBalancer + * Properties of a discovered load balancer */ export interface LoadBalancerContextResponse { /** @@ -49,7 +49,7 @@ export interface LoadBalancerContextResponse { } /** - * Properties of a discovered ApplicationListener. + * Properties of a discovered load balancer listener. */ export interface LoadBalancerListenerContextResponse { /** From 83a9dd9af81ade4b9aaa541a5ca24fe8c17853ac Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 10:01:36 -0600 Subject: [PATCH 15/51] fix: describeLoadBalancers missing NextMarker --- packages/aws-cdk/lib/context-providers/load-balancers.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index d26c0192e5134..3d5f33b64428e 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -170,9 +170,12 @@ async function findLoadBalancers(elbv2: AWS.ELBv2, args: cxschema.LoadBalancerFi */ async function describeLoadBalancers(elbv2: AWS.ELBv2, request: AWS.ELBv2.DescribeLoadBalancersInput) { const loadBalancers = Array(); - let page: AWS.ELBv2.DescribeLoadBalancersOutput; + let page: AWS.ELBv2.DescribeLoadBalancersOutput | undefined; do { - page = await elbv2.describeLoadBalancers(request).promise(); + page = await elbv2.describeLoadBalancers({ + ...request, + Marker: page ? page.NextMarker : undefined, + }).promise(); const pageItems = page.LoadBalancers ?? []; for (const loadBalancer of pageItems) { loadBalancers.push(loadBalancer); From f3b3d927716de58c45650611a84e915512ca8032 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 10:04:38 -0600 Subject: [PATCH 16/51] fix: security group should check for tokens --- packages/@aws-cdk/aws-ec2/lib/security-group.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index 912c29118b58d..1f06da47c7e72 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -300,6 +300,10 @@ export class SecurityGroup extends SecurityGroupBase { * Look up a security group by id. */ static fromLookup(scope: Construct, id: string, securityGroupId: string) { + if (Token.isUnresolved(securityGroupId)) { + throw new Error('All arguments to look up a security group must be concrete (no Tokens)'); + } + const attributes: cxapi.SecurityGroupContextResponse = ContextProvider.getValue(scope, { provider: cxschema.ContextProvider.SECURITY_GROUP_PROVIDER, props: { From 79a81ec07524a4f93cef861c329ed04f9aa05a58 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 10:07:05 -0600 Subject: [PATCH 17/51] style: remove securityGroupId key --- packages/@aws-cdk/aws-ec2/lib/security-group.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index 1f06da47c7e72..b1d77051d7ddb 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -306,9 +306,7 @@ export class SecurityGroup extends SecurityGroupBase { const attributes: cxapi.SecurityGroupContextResponse = ContextProvider.getValue(scope, { provider: cxschema.ContextProvider.SECURITY_GROUP_PROVIDER, - props: { - securityGroupId: securityGroupId, - } as cxschema.SecurityGroupContextQuery, + props: { securityGroupId }, dummyValue: undefined, }).value; From 2bc77508754cdb719f3cff61705b6b918c0ebbca Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 10:20:48 -0600 Subject: [PATCH 18/51] fix: dummy attributes should be obviously false --- packages/@aws-cdk/aws-ec2/lib/security-group.ts | 2 +- packages/@aws-cdk/aws-ec2/test/security-group.test.ts | 2 +- .../aws-elasticloadbalancingv2/test/alb/listener.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index b1d77051d7ddb..4ba226f6ca608 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -312,7 +312,7 @@ export class SecurityGroup extends SecurityGroupBase { // Dummy attributes for tests and the first pass. const dummyAttributes: cxapi.SecurityGroupContextResponse = { - securityGroupId: securityGroupId, + securityGroupId: 'sg-12345', allowAllOutbound: true, }; diff --git a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts index e6e18832d99ad..1a72a6a715d2c 100644 --- a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts @@ -305,7 +305,7 @@ nodeunitShim({ const securityGroup = SecurityGroup.fromLookup(stack, 'stack', 'sg-1234'); - test.equal(securityGroup.securityGroupId, 'sg-1234'); + test.equal(securityGroup.securityGroupId, 'sg-12345'); test.equal(securityGroup.allowAllOutbound, true); test.done(); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts index 84f91967c8c6b..b9de0961423ec 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -1386,7 +1386,7 @@ describe('tests', () => { // THEN expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); expect(listener.listenerArn).toEqual('arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/application/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2'); - expect(listener.connections.securityGroups[0].securityGroupId).toEqual('sg-123456789012'); + expect(listener.connections.securityGroups[0].securityGroupId).toEqual('sg-12345'); }); test('Can add rules to a looked-up ApplicationListener', () => { From 0cc3f6a33fb19e33c22e6bf7bf520edf75b552a8 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 10:51:22 -0600 Subject: [PATCH 19/51] refactor: use dummyValue getValue input --- .../@aws-cdk/aws-ec2/lib/security-group.ts | 17 ++++++---------- .../lib/shared/base-listener.ts | 14 ++++++------- .../lib/shared/base-load-balancer.ts | 20 +++++++++---------- 3 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index 4ba226f6ca608..33d6666a0ef9d 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -307,19 +307,14 @@ export class SecurityGroup extends SecurityGroupBase { const attributes: cxapi.SecurityGroupContextResponse = ContextProvider.getValue(scope, { provider: cxschema.ContextProvider.SECURITY_GROUP_PROVIDER, props: { securityGroupId }, - dummyValue: undefined, + dummyValue: { + securityGroupId: 'sg-12345', + allowAllOutbound: true, + } as cxapi.SecurityGroupContextResponse, }).value; - // Dummy attributes for tests and the first pass. - const dummyAttributes: cxapi.SecurityGroupContextResponse = { - securityGroupId: 'sg-12345', - allowAllOutbound: true, - }; - - const attributesToUse = attributes ?? dummyAttributes; - - return SecurityGroup.fromSecurityGroupId(scope, id, attributesToUse.securityGroupId, { - allowAllOutbound: attributesToUse.allowAllOutbound, + return SecurityGroup.fromSecurityGroupId(scope, id, attributes.securityGroupId, { + allowAllOutbound: attributes.allowAllOutbound, mutable: true, }); } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index 710542804fed2..d0d2fc763f93e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -79,16 +79,14 @@ export abstract class BaseListener extends Resource { loadBalancerType: options.loadBalancerType, listenerProtocol: options.listenerProtocol, } as cxschema.LoadBalancerListenerContextQuery, - dummyValue: undefined, + dummyValue: { + listenerArn: `arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/${options.loadBalancerType}/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2`, + listenerPort: 80, + securityGroupIds: ['sg-123456789012'], + } as cxapi.LoadBalancerListenerContextResponse, }).value; - const defaultProps: cxapi.LoadBalancerListenerContextResponse = { - listenerArn: `arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/${options.loadBalancerType}/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2`, - listenerPort: 80, - securityGroupIds: ['sg-123456789012'], - }; - - return props ?? defaultProps; + return props; } /** * @attribute diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts index 0cbeba29510d3..1fb7ce6581099 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts @@ -119,19 +119,17 @@ export abstract class BaseLoadBalancer extends Resource { ...options.userOptions, loadBalancerType: options.loadBalancerType, } as cxschema.LoadBalancerContextQuery, - dummyValue: undefined, + dummyValue: { + ipAddressType: cxapi.LoadBalancerIpAddressType.DUAL_STACK, + loadBalancerArn: `arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/${options.loadBalancerType}/my-load-balancer/50dc6c495c0c9188`, + loadBalancerCanonicalHostedZoneId: 'Z3DZXE0EXAMPLE', + loadBalancerDnsName: 'my-load-balancer-1234567890.us-west-2.elb.amazonaws.com', + securityGroupIds: ['sg-1234'], + vpcId: 'vpc-12345', + } as cxapi.LoadBalancerContextResponse, }).value; - const dummyProps: cxapi.LoadBalancerContextResponse = { - ipAddressType: cxapi.LoadBalancerIpAddressType.DUAL_STACK, - loadBalancerArn: `arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/${options.loadBalancerType}/my-load-balancer/50dc6c495c0c9188`, - loadBalancerCanonicalHostedZoneId: 'Z3DZXE0EXAMPLE', - loadBalancerDnsName: 'my-load-balancer-1234567890.us-west-2.elb.amazonaws.com', - securityGroupIds: ['sg-1234'], - vpcId: 'vpc-12345', - }; - - return props ?? dummyProps; + return props; } /** From 1c71d2168ced16b06774de7a62a3889bf0f732e1 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 10:57:13 -0600 Subject: [PATCH 20/51] fix: use === and \!== throughout --- .../lib/alb/application-listener.ts | 2 +- .../lib/alb/application-load-balancer.ts | 4 ++-- packages/aws-cdk/lib/context-providers/load-balancers.ts | 8 ++++---- packages/aws-cdk/lib/context-providers/security-groups.ts | 2 +- .../aws-cdk/test/context-providers/load-balancers.test.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 55e25910ac284..1145239656d24 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -132,7 +132,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis const props = BaseListener._queryContextProvider(scope, { userOptions: options, loadBalancerType: cxschema.LoadBalancerType.APPLICATION, - listenerProtocol: options.listenerProtocol == ApplicationProtocol.HTTP + listenerProtocol: options.listenerProtocol === ApplicationProtocol.HTTP ? cxschema.LoadBalancerListenerProtocol.HTTP : cxschema.LoadBalancerListenerProtocol.HTTPS, }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index 73266c7577e65..544c594cdc44c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -633,9 +633,9 @@ class LookedUpApplicationLoadBalancer extends Resource implements IApplicationLo this.loadBalancerCanonicalHostedZoneId = props.loadBalancerCanonicalHostedZoneId; this.loadBalancerDnsName = props.loadBalancerDnsName; - if (props.ipAddressType == cxapi.LoadBalancerIpAddressType.IPV4) { + if (props.ipAddressType === cxapi.LoadBalancerIpAddressType.IPV4) { this.ipAddressType = IpAddressType.IPV4; - } else if (props.ipAddressType == cxapi.LoadBalancerIpAddressType.DUAL_STACK) { + } else if (props.ipAddressType === cxapi.LoadBalancerIpAddressType.DUAL_STACK) { this.ipAddressType = IpAddressType.DUAL_STACK; } diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index 3d5f33b64428e..95c7ba32456a9 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -29,7 +29,7 @@ export class LoadBalancerContextProviderPlugin implements ContextProviderPlugin const loadBalancer = loadBalancers[0]; - const ipAddressType = loadBalancer.IpAddressType == 'ipv4' + const ipAddressType = loadBalancer.IpAddressType === 'ipv4' ? cxapi.LoadBalancerIpAddressType.IPV4 : cxapi.LoadBalancerIpAddressType.DUAL_STACK; @@ -154,7 +154,7 @@ async function findLoadBalancers(elbv2: AWS.ELBv2, args: cxschema.LoadBalancerFi // Filter by load balancer type if (args.loadBalancerType) { - loadBalancers = loadBalancers.filter(lb => lb.Type == args.loadBalancerType); + loadBalancers = loadBalancers.filter(lb => lb.Type === args.loadBalancerType); } // Filter by load balancer tags @@ -268,12 +268,12 @@ async function* describeListenersByLoadBalancerArn(elbv2: AWS.ELBv2, loadBalance * Determines if a listener matches the query filters. */ function listenerMatchesQueryFilter(listener: AWS.ELBv2.Listener, args: cxschema.LoadBalancerListenerContextQuery): boolean { - if (args.listenerPort && listener.Port != args.listenerPort) { + if (args.listenerPort && listener.Port !== args.listenerPort) { // No match. return false; } - if (args.listenerProtocol && listener.Protocol != args.listenerProtocol) { + if (args.listenerProtocol && listener.Protocol !== args.listenerProtocol) { // No match. return false; } diff --git a/packages/aws-cdk/lib/context-providers/security-groups.ts b/packages/aws-cdk/lib/context-providers/security-groups.ts index d1cda6d070626..4133f8a6b60bd 100644 --- a/packages/aws-cdk/lib/context-providers/security-groups.ts +++ b/packages/aws-cdk/lib/context-providers/security-groups.ts @@ -38,7 +38,7 @@ export class SecurityGroupContextProviderPlugin implements ContextProviderPlugin export function hasAllTrafficEgress(securityGroup: AWS.EC2.SecurityGroup) { for (const ipPermission of securityGroup.IpPermissionsEgress ?? []) { const hasAllTrafficCidr = Boolean(ipPermission.IpRanges?.find(m => m.CidrIp === '0.0.0.0/0')); - const isAllProtocols = ipPermission.IpProtocol == '-1'; + const isAllProtocols = ipPermission.IpProtocol === '-1'; if (hasAllTrafficCidr && isAllProtocols) { return true; diff --git a/packages/aws-cdk/test/context-providers/load-balancers.test.ts b/packages/aws-cdk/test/context-providers/load-balancers.test.ts index bf91a12b7be0a..b6235da556650 100644 --- a/packages/aws-cdk/test/context-providers/load-balancers.test.ts +++ b/packages/aws-cdk/test/context-providers/load-balancers.test.ts @@ -375,7 +375,7 @@ describe('load balancer listener context provider plugin', () => { }); AWS.mock('ELBv2', 'describeListeners', (params: aws.ELBv2.DescribeListenersInput, cb: AwsCallback) => { - if (params.LoadBalancerArn == 'arn:load-balancer1') { + if (params.LoadBalancerArn === 'arn:load-balancer1') { cb(null, { Listeners: [ { @@ -401,7 +401,7 @@ describe('load balancer listener context provider plugin', () => { }, ], }); - } else if (params.LoadBalancerArn == 'arn:load-balancer2') { + } else if (params.LoadBalancerArn === 'arn:load-balancer2') { cb(null, { Listeners: [ { From 6109fe3ba262841e1bebb7e4abf2ba44a92bbe2b Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 11:05:23 -0600 Subject: [PATCH 21/51] refactor: keep connections and listenerArn abstract --- .../lib/alb/application-listener.ts | 52 ++++++++----------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 1145239656d24..3e41de3e9bd5f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -545,37 +545,19 @@ export interface ApplicationListenerAttributes { readonly securityGroupAllowsAllOutbound?: boolean; } -/** - * Props for `ImportedApplicationListenerBase` - */ -interface ImportedApplicationListenerBaseProps { - /** - * Arn of the listener. - */ - readonly listenerArn: string; - - /** - * Connections object. - */ - readonly connections: ec2.Connections; -} - abstract class ImportedApplicationListenerBase extends Resource implements IApplicationListener { /** * Connections object. */ - public readonly connections: ec2.Connections; + public abstract readonly connections: ec2.Connections; /** * ARN of the listener */ - public readonly listenerArn: string; + public abstract readonly listenerArn: string; - constructor(scope: Construct, id: string, props: ImportedApplicationListenerBaseProps) { + constructor(scope: Construct, id: string) { super(scope, id); - - this.connections = props.connections; - this.listenerArn = props.listenerArn; } /** @@ -643,14 +625,14 @@ abstract class ImportedApplicationListenerBase extends Resource implements IAppl * An imported application listener. */ class ImportedApplicationListener extends ImportedApplicationListenerBase { + public readonly listenerArn: string; + public readonly connections: ec2.Connections; + constructor(scope: Construct, id: string, props: ApplicationListenerAttributes) { - const defaultPort = props.defaultPort !== undefined ? ec2.Port.tcp(props.defaultPort) : undefined; - const connections = new ec2.Connections({ defaultPort }); + super(scope, id); - super(scope, id, { - listenerArn: props.listenerArn, - connections, - }); + this.listenerArn = props.listenerArn; + const defaultPort = props.defaultPort !== undefined ? ec2.Port.tcp(props.defaultPort) : undefined; let securityGroup: ec2.ISecurityGroup; if (props.securityGroup) { @@ -663,15 +645,23 @@ class ImportedApplicationListener extends ImportedApplicationListenerBase { throw new Error('Either `securityGroup` or `securityGroupId` must be specified to import an application listener.'); } - this.connections.addSecurityGroup(securityGroup); + this.connections = new ec2.Connections({ + securityGroups: [securityGroup], + defaultPort, + }); } } class LookedUpApplicationListener extends ImportedApplicationListenerBase { + public readonly listenerArn: string; + public readonly connections: ec2.Connections; + constructor(scope: Construct, id: string, props: cxapi.LoadBalancerListenerContextResponse) { - super(scope, id, { - listenerArn: props.listenerArn, - connections: new ec2.Connections({ defaultPort: ec2.Port.tcp(props.listenerPort) }), + super(scope, id); + + this.listenerArn = props.listenerArn; + this.connections = new ec2.Connections({ + defaultPort: ec2.Port.tcp(props.listenerPort), }); for (const securityGroupId of props.securityGroupIds) { From e0707afe3d8796725d18cfeb48012c9d2034e109 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 11:07:21 -0600 Subject: [PATCH 22/51] fix: unnecessary assignment to undefined --- .../aws-elasticloadbalancingv2/lib/nlb/network-listener.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts index bb7c18919e487..cb5fac291e602 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts @@ -108,7 +108,7 @@ export class NetworkListener extends BaseListener implements INetworkListener { * Looks up a network listener */ public static fromLookup(scope: Construct, id: string, options: NetworkListenerLookupOptions): INetworkListener { - let listenerProtocol: cxschema.LoadBalancerListenerProtocol | undefined = undefined; + let listenerProtocol: cxschema.LoadBalancerListenerProtocol | undefined; if (options.listenerProtocol) { validateNetworkProtocol(options.listenerProtocol); From 0e5f494cbf6339f01b69bbc5b086730e5fc7d9c9 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 11:29:39 -0600 Subject: [PATCH 23/51] fix: docstring references to ApplicationListener in base-listener --- .../aws-elasticloadbalancingv2/lib/shared/base-listener.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index d0d2fc763f93e..ba6ced8667e1d 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -6,11 +6,11 @@ import { CfnListener } from '../elasticloadbalancingv2.generated'; import { IListenerAction } from './listener-action'; /** - * Options for ApplicationListener lookup + * Options for listener lookup */ export interface BaseListenerLookupOptions { /** - * ARN of the application listener to look up + * ARN of the listener to look up * @default - does not filter by listener arn */ readonly listenerArn?: string; From f7c50c5b52734d5c02026a87623dbc599cbbe2d4 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 11:54:57 -0600 Subject: [PATCH 24/51] style: spacing on multiline conditionals --- .../aws-elasticloadbalancingv2/lib/shared/base-listener.ts | 6 +++--- .../lib/shared/base-load-balancer.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index ba6ced8667e1d..30bf85769a25c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -66,9 +66,9 @@ export abstract class BaseListener extends Resource { */ protected static _queryContextProvider(scope: Construct, options: ListenerQueryContextProviderOptions) { if (Token.isUnresolved(options.userOptions.loadBalancerArn) - ||Token.isUnresolved(options.userOptions.loadBalancerTags) - ||Token.isUnresolved(options.userOptions.listenerArn) - ||Token.isUnresolved(options.userOptions.listenerPort)) { + || Token.isUnresolved(options.userOptions.loadBalancerTags) + || Token.isUnresolved(options.userOptions.listenerArn) + || Token.isUnresolved(options.userOptions.listenerPort)) { throw new Error('All arguments to look up a load balancer listener must be concrete (no Tokens)'); } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts index 1fb7ce6581099..012987b6c3fed 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts @@ -109,7 +109,7 @@ export abstract class BaseLoadBalancer extends Resource { */ protected static _queryContextProvider(scope: Construct, options: LoadBalancerQueryContextProviderOptions) { if (Token.isUnresolved(options.userOptions.loadBalancerArn) - ||Token.isUnresolved(options.userOptions.loadBalancerTags)) { + || Token.isUnresolved(options.userOptions.loadBalancerTags)) { throw new Error('All arguments to look up a load balancer must be concrete (no Tokens)'); } From da69e9548277b486a926d3a90e655209100c014b Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 12:28:47 -0600 Subject: [PATCH 25/51] refactor: reuse existing Tag type --- .../lib/shared/base-listener.ts | 2 +- .../lib/shared/base-load-balancer.ts | 2 +- .../test/alb/listener.test.ts | 18 +++++++++--------- .../test/alb/load-balancer.test.ts | 12 ++++++------ .../test/nlb/listener.test.ts | 6 +++--- .../test/nlb/load-balancer.test.ts | 12 ++++++------ .../lib/cloud-assembly/context-queries.ts | 3 ++- .../schema/cloud-assembly.schema.json | 13 ++++++++----- .../lib/context-providers/load-balancers.ts | 8 ++++---- .../context-providers/load-balancers.test.ts | 14 +++++++------- 10 files changed, 47 insertions(+), 43 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index 30bf85769a25c..2cf7211184921 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -25,7 +25,7 @@ export interface BaseListenerLookupOptions { * Filter listeners by associated load balancer tags * @default - does not filter by load balancer tags */ - readonly loadBalancerTags?: Record; + readonly loadBalancerTags?: cxschema.Tag[]; /** * Filter listeners by listener port diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts index 012987b6c3fed..03d975e787820 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts @@ -80,7 +80,7 @@ export interface BaseLoadBalancerLookupOptions { * Match load balancer tags. * @default - does not match load balancers by tags */ - readonly loadBalancerTags?: Record; + readonly loadBalancerTags?: cxschema.Tag[]; } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts index b9de0961423ec..23e0e15186874 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -1378,9 +1378,9 @@ describe('tests', () => { // WHEN const listener = elbv2.ApplicationListener.fromLookup(stack, 'a', { - loadBalancerTags: { - some: 'tag', - }, + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + ], }); // THEN @@ -1400,9 +1400,9 @@ describe('tests', () => { }); const listener = elbv2.ApplicationListener.fromLookup(stack, 'a', { - loadBalancerTags: { - some: 'tag', - }, + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + ], }); // WHEN @@ -1432,9 +1432,9 @@ describe('tests', () => { }); const listener = elbv2.ApplicationListener.fromLookup(stack, 'a', { - loadBalancerTags: { - some: 'tag', - }, + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + ], }); // WHEN diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts index e2745a2fb02aa..5f1dd1b0aeb0c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts @@ -348,9 +348,9 @@ describe('tests', () => { // WHEN const loadBalancer = elbv2.ApplicationLoadBalancer.fromLookup(stack, 'a', { - loadBalancerTags: { - some: 'tag', - }, + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + ], }); // THEN @@ -373,9 +373,9 @@ describe('tests', () => { }); const loadBalancer = elbv2.ApplicationLoadBalancer.fromLookup(stack, 'a', { - loadBalancerTags: { - some: 'tag', - }, + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + ], }); // WHEN diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts index d40197d00a280..e3042eec5bbb9 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts @@ -401,9 +401,9 @@ describe('tests', () => { // WHEN const listener = elbv2.NetworkListener.fromLookup(stack, 'a', { - loadBalancerTags: { - some: 'tag', - }, + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + ], }); // THEN diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts index 88a3999ec0a6f..f0b5c3cbf80aa 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts @@ -415,9 +415,9 @@ describe('tests', () => { // WHEN const loadBalancer = elbv2.NetworkLoadBalancer.fromLookup(stack, 'a', { - loadBalancerTags: { - some: 'tag', - }, + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + ], }); // THEN @@ -438,9 +438,9 @@ describe('tests', () => { }); const loadBalancer = elbv2.NetworkLoadBalancer.fromLookup(stack, 'a', { - loadBalancerTags: { - some: 'tag', - }, + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + ], }); const targetGroup = new elbv2.NetworkTargetGroup(stack, 'tg', { diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts index 37de5860f4e85..58de6db0b2417 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts @@ -1,3 +1,4 @@ +import { Tag } from './metadata-schema'; /** * Identifier for the context provider @@ -245,7 +246,7 @@ export interface LoadBalancerFilter { * Match load balancer tags * @default - does not match load balancers by tags */ - readonly loadBalancerTags?: Record; + readonly loadBalancerTags?: Tag[]; } /** diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index 398de4f0eeffa..9cb10e76e4c56 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -618,7 +618,10 @@ }, "loadBalancerTags": { "description": "Match load balancer tags (Default - does not match load balancers by tags)", - "$ref": "#/definitions/Record" + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + } } }, "required": [ @@ -626,9 +629,6 @@ "region" ] }, - "Record": { - "type": "object" - }, "LoadBalancerListenerContextQuery": { "description": "Query input for looking up a load balancer listener", "type": "object", @@ -675,7 +675,10 @@ }, "loadBalancerTags": { "description": "Match load balancer tags (Default - does not match load balancers by tags)", - "$ref": "#/definitions/Record" + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + } } }, "required": [ diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index 95c7ba32456a9..a726b8561bbcd 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -189,7 +189,7 @@ async function describeLoadBalancers(elbv2: AWS.ELBv2, request: AWS.ELBv2.Descri * Describes the tags of each load balancer and returns the load balancers that * match the given tags. */ -async function filterLoadBalancersByTags(elbv2: AWS.ELBv2, loadBalancers: AWS.ELBv2.LoadBalancers, loadBalancerTags: Record) { +async function filterLoadBalancersByTags(elbv2: AWS.ELBv2, loadBalancers: AWS.ELBv2.LoadBalancers, loadBalancerTags: cxschema.Tag[]) { const loadBalancersByArn = indexLoadBalancersByArn(loadBalancers); const loadBalancerArns = Object.keys(loadBalancersByArn); const matchingLoadBalancers = Array(); @@ -227,14 +227,14 @@ async function* describeTags(elbv2: AWS.ELBv2, resourceArns: string[]) { /** * Determines if the given TagDescription matches the required tags. */ -function tagsMatch(tagDescription: AWS.ELBv2.TagDescription, requiredTags: Record) { +function tagsMatch(tagDescription: AWS.ELBv2.TagDescription, requiredTags: cxschema.Tag[]) { const tagsByName: Record = {}; for (const tag of tagDescription.Tags ?? []) { tagsByName[tag.Key!] = tag.Value!; } - for (const [requiredTag, requiredValue] of Object.entries(requiredTags)) { - if (tagsByName[requiredTag] !== requiredValue) { + for (const tag of requiredTags) { + if (tagsByName[tag.key] !== tag.value) { return false; } } diff --git a/packages/aws-cdk/test/context-providers/load-balancers.test.ts b/packages/aws-cdk/test/context-providers/load-balancers.test.ts index b6235da556650..c6b2bf26152af 100644 --- a/packages/aws-cdk/test/context-providers/load-balancers.test.ts +++ b/packages/aws-cdk/test/context-providers/load-balancers.test.ts @@ -113,10 +113,10 @@ describe('load balancer context provider plugin', () => { await provider.getValue({ account: '1234', region: 'us-east-1', - loadBalancerTags: { - some: 'tag', - second: 'tag2', - }, + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + { key: 'second', value: 'tag2' }, + ], }); }); @@ -332,9 +332,9 @@ describe('load balancer listener context provider plugin', () => { const listener = await provider.getValue({ account: '1234', region: 'us-east-1', - loadBalancerTags: { - some: 'tag', - }, + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + ], listenerPort: 999, }); From a2bdb637073b6ebc489ef9969a9db528a67820e4 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 12:34:50 -0600 Subject: [PATCH 26/51] refactor: simplify LB context provider --- .../lib/context-providers/load-balancers.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index a726b8561bbcd..ac54fbe98df73 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -4,10 +4,6 @@ import * as AWS from 'aws-sdk'; import { Mode, SdkProvider } from '../api'; import { ContextProviderPlugin } from './provider'; -// Decreases line length -type LoadBalancerQuery = cxschema.LoadBalancerContextQuery; -type LoadBalancerResponse = cxapi.LoadBalancerContextResponse; - /** * Provides load balancer context information. */ @@ -15,11 +11,8 @@ export class LoadBalancerContextProviderPlugin implements ContextProviderPlugin constructor(private readonly aws: SdkProvider) { } - async getValue(query: LoadBalancerQuery): Promise { - const account: string = query.account!; - const region: string = query.region!; - - const elbv2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(account, region), Mode.ForReading)).elbv2(); + async getValue(query: cxschema.LoadBalancerContextQuery): Promise { + const elbv2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(query.account, query.region), Mode.ForReading)).elbv2(); const loadBalancers = await findLoadBalancers(elbv2, query); @@ -56,10 +49,7 @@ export class LoadBalancerListenerContextProviderPlugin implements ContextProvide } async getValue(query: LoadBalancerListenerQuery): Promise { - const account: string = query.account!; - const region: string = query.region!; - - const elbv2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(account, region), Mode.ForReading)).elbv2(); + const elbv2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(query.account, query.region), Mode.ForReading)).elbv2(); if (query.listenerArn) { // When we know a listener arn, we can query for that listener directly. From 3e18ef49038cd05800c82510d17a220fa90816cb Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 12:36:08 -0600 Subject: [PATCH 27/51] refactor: getListenerDirectly -> getListenerByArn --- packages/aws-cdk/lib/context-providers/load-balancers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index ac54fbe98df73..1eafffbfbb399 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -53,7 +53,7 @@ export class LoadBalancerListenerContextProviderPlugin implements ContextProvide if (query.listenerArn) { // When we know a listener arn, we can query for that listener directly. - return this.getListenerDirectly(elbv2, query); + return this.getListenerByArn(elbv2, query); } else { // When we don't know a listener arn, we need to find it by traversing // all associated load balancers. @@ -65,7 +65,7 @@ export class LoadBalancerListenerContextProviderPlugin implements ContextProvide * Look up a listener by querying listeners for query's listener arn and then * resolve its load balancer for the security group information. */ - private async getListenerDirectly(elbv2: AWS.ELBv2, query: LoadBalancerListenerQuery) { + private async getListenerByArn(elbv2: AWS.ELBv2, query: LoadBalancerListenerQuery) { const listenerArn = query.listenerArn!; const listenerResults = await elbv2.describeListeners({ ListenerArns: [listenerArn] }).promise(); const listeners = (listenerResults.Listeners ?? []); From c5ec92b53b36e6e252d95283c775460b3a11de82 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 12:57:29 -0600 Subject: [PATCH 28/51] fix: listener matching should be deterministic --- .../lib/context-providers/load-balancers.ts | 25 ++++++++--- .../context-providers/load-balancers.test.ts | 41 +++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index 1eafffbfbb399..8e4146959a5eb 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -108,28 +108,41 @@ export class LoadBalancerListenerContextProviderPlugin implements ContextProvide } // Find the first matching listener - return this.findFirstMatchingListener(elbv2, loadBalancers, args); + return this.findMatchingListener(elbv2, loadBalancers, args); } /** - * Finds the first matching listener from the list of load balancers. + * Finds the matching listener from the list of load balancers. This will + * error unless there is exactly one match so that the user is prompted to + * provide more specific criteria rather than us providing a nondeterministic + * result. */ - private async findFirstMatchingListener(elbv2: AWS.ELBv2, loadBalancers: AWS.ELBv2.LoadBalancers, query: LoadBalancerListenerQuery) { + private async findMatchingListener(elbv2: AWS.ELBv2, loadBalancers: AWS.ELBv2.LoadBalancers, query: LoadBalancerListenerQuery) { const loadBalancersByArn = indexLoadBalancersByArn(loadBalancers); const loadBalancerArns = Object.keys(loadBalancersByArn); + const matches = Array(); + for await (const listener of describeListenersByLoadBalancerArn(elbv2, loadBalancerArns)) { const loadBalancer = loadBalancersByArn[listener.LoadBalancerArn!]; if (listenerMatchesQueryFilter(listener, query) && loadBalancer) { - return { + matches.push({ listenerArn: listener.ListenerArn!, listenerPort: listener.Port!, securityGroupIds: loadBalancer.SecurityGroups ?? [], - }; + }); } } - throw new Error(`No load balancer listeners found matching ${JSON.stringify(query)}`); + if (matches.length === 0) { + throw new Error(`No load balancer listeners found matching ${JSON.stringify(query)}`); + } + + if (matches.length > 1) { + throw new Error(`Multiple load balancer listeners found matching ${JSON.stringify(query)} - please provide more specific criteria`); + } + + return matches[0]; } } diff --git a/packages/aws-cdk/test/context-providers/load-balancers.test.ts b/packages/aws-cdk/test/context-providers/load-balancers.test.ts index c6b2bf26152af..bd4aeb40c0ea8 100644 --- a/packages/aws-cdk/test/context-providers/load-balancers.test.ts +++ b/packages/aws-cdk/test/context-providers/load-balancers.test.ts @@ -188,6 +188,7 @@ describe('load balancer listener context provider plugin', () => { ], listeners: [ { + LoadBalancerArn: 'arn:load-balancer', ListenerArn: 'arn:listener', Port: 80, Protocol: 'HTTP', @@ -206,6 +207,46 @@ describe('load balancer listener context provider plugin', () => { ).rejects.toThrow(/No load balancer listeners found/i); }); + test('errors when multiple listeners match', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [ + { + LoadBalancerArn: 'arn:load-balancer', + }, + { + LoadBalancerArn: 'arn:load-balancer2', + }, + ], + listeners: [ + { + LoadBalancerArn: 'arn:load-balancer', + ListenerArn: 'arn:listener', + Port: 80, + Protocol: 'HTTP', + }, + { + LoadBalancerArn: 'arn:load-balancer2', + ListenerArn: 'arn:listener2', + Port: 80, + Protocol: 'HTTP', + }, + ], + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + listenerPort: 80, + listenerProtocol: cxschema.LoadBalancerListenerProtocol.HTTP, + }), + ).rejects.toThrow(/Multiple load balancer listeners/i); + }); + test('looks up by listener arn', async () => { // GIVEN const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); From f1eadc66b98c823c1a64e37c424fcda3a541b810 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 13:02:13 -0600 Subject: [PATCH 29/51] fix: load balancer lookup should be deterministic --- .../lib/context-providers/load-balancers.ts | 4 ++ .../context-providers/load-balancers.test.ts | 52 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index 8e4146959a5eb..e39c50908b440 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -20,6 +20,10 @@ export class LoadBalancerContextProviderPlugin implements ContextProviderPlugin throw new Error(`No load balancers found matching ${JSON.stringify(query)}`); } + if (loadBalancers.length > 1) { + throw new Error(`Multiple load balancers found matching ${JSON.stringify(query)} - please provide more specific criteria`); + } + const loadBalancer = loadBalancers[0]; const ipAddressType = loadBalancer.IpAddressType === 'ipv4' diff --git a/packages/aws-cdk/test/context-providers/load-balancers.test.ts b/packages/aws-cdk/test/context-providers/load-balancers.test.ts index bd4aeb40c0ea8..42481e313fdef 100644 --- a/packages/aws-cdk/test/context-providers/load-balancers.test.ts +++ b/packages/aws-cdk/test/context-providers/load-balancers.test.ts @@ -34,6 +34,58 @@ describe('load balancer context provider plugin', () => { ).rejects.toThrow(/No load balancers found/i); }); + test('errors when multiple load balancers match', async () => { + // GIVEN + const provider = new LoadBalancerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [ + { + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer1', + DNSName: 'dns1.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + }, + { + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer2', + DNSName: 'dns2.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + }, + ], + describeTagsExpected: { ResourceArns: ['arn:load-balancer1', 'arn:load-balancer2'] }, + tagDescriptions: [ + { + ResourceArn: 'arn:load-balancer1', + Tags: [ + { Key: 'some', Value: 'tag' }, + ], + }, + { + ResourceArn: 'arn:load-balancer2', + Tags: [ + { Key: 'some', Value: 'tag' }, + ], + }, + ], + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + ], + }), + ).rejects.toThrow(/Multiple load balancers found/i); + }); + test('looks up by arn', async () => { // GIVEN const provider = new LoadBalancerContextProviderPlugin(mockSDK); From 5c851d290a2e25ade7389bc44ad1470519657ca4 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 13:20:20 -0600 Subject: [PATCH 30/51] fix: load balancer lookup should error when missing filters --- .../lib/cloud-assembly/context-queries.ts | 3 +-- .../schema/cloud-assembly.schema.json | 26 ++++++++++--------- .../lib/context-providers/load-balancers.ts | 8 +++--- .../context-providers/load-balancers.test.ts | 25 +++++++++++++++++- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts index 58de6db0b2417..56c609a08dd39 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts @@ -232,9 +232,8 @@ export enum LoadBalancerType { export interface LoadBalancerFilter { /** * Filter load balancers by their type - * @default - does not filter by load balancer type */ - readonly loadBalancerType?: LoadBalancerType; + readonly loadBalancerType: LoadBalancerType; /** * Find by load balancer's ARN diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index 9cb10e76e4c56..99fbaedb6c416 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -605,12 +605,8 @@ "type": "string" }, "loadBalancerType": { - "description": "Filter load balancers by their type (Default - does not filter by load balancer type)", - "enum": [ - "application", - "network" - ], - "type": "string" + "$ref": "#/definitions/LoadBalancerType", + "description": "Filter load balancers by their type" }, "loadBalancerArn": { "description": "Find by load balancer's ARN (Default - does not search by load balancer arn)", @@ -626,9 +622,18 @@ }, "required": [ "account", + "loadBalancerType", "region" ] }, + "LoadBalancerType": { + "description": "Type of load balancer", + "enum": [ + "application", + "network" + ], + "type": "string" + }, "LoadBalancerListenerContextQuery": { "description": "Query input for looking up a load balancer listener", "type": "object", @@ -662,12 +667,8 @@ "type": "number" }, "loadBalancerType": { - "description": "Filter load balancers by their type (Default - does not filter by load balancer type)", - "enum": [ - "application", - "network" - ], - "type": "string" + "$ref": "#/definitions/LoadBalancerType", + "description": "Filter load balancers by their type" }, "loadBalancerArn": { "description": "Find by load balancer's ARN (Default - does not search by load balancer arn)", @@ -683,6 +684,7 @@ }, "required": [ "account", + "loadBalancerType", "region" ] }, diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index e39c50908b440..cd12aebd2c2f8 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -14,6 +14,10 @@ export class LoadBalancerContextProviderPlugin implements ContextProviderPlugin async getValue(query: cxschema.LoadBalancerContextQuery): Promise { const elbv2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(query.account, query.region), Mode.ForReading)).elbv2(); + if (!query.loadBalancerArn && !query.loadBalancerTags) { + throw new Error('The load balancer lookup query must specify either `loadBalancerArn` or `loadBalancerTags`'); + } + const loadBalancers = await findLoadBalancers(elbv2, query); if (loadBalancers.length === 0) { @@ -160,9 +164,7 @@ async function findLoadBalancers(elbv2: AWS.ELBv2, args: cxschema.LoadBalancerFi }); // Filter by load balancer type - if (args.loadBalancerType) { - loadBalancers = loadBalancers.filter(lb => lb.Type === args.loadBalancerType); - } + loadBalancers = loadBalancers.filter(lb => lb.Type === args.loadBalancerType); // Filter by load balancer tags if (args.loadBalancerTags) { diff --git a/packages/aws-cdk/test/context-providers/load-balancers.test.ts b/packages/aws-cdk/test/context-providers/load-balancers.test.ts index 42481e313fdef..ab276e2c3c469 100644 --- a/packages/aws-cdk/test/context-providers/load-balancers.test.ts +++ b/packages/aws-cdk/test/context-providers/load-balancers.test.ts @@ -29,6 +29,7 @@ describe('load balancer context provider plugin', () => { provider.getValue({ account: '1234', region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, loadBalancerArn: 'arn:load-balancer1', }), ).rejects.toThrow(/No load balancers found/i); @@ -47,6 +48,7 @@ describe('load balancer context provider plugin', () => { CanonicalHostedZoneId: 'Z1234', SecurityGroups: ['sg-1234'], VpcId: 'vpc-1234', + Type: 'application', }, { IpAddressType: 'ipv4', @@ -55,6 +57,7 @@ describe('load balancer context provider plugin', () => { CanonicalHostedZoneId: 'Z1234', SecurityGroups: ['sg-1234'], VpcId: 'vpc-1234', + Type: 'application', }, ], describeTagsExpected: { ResourceArns: ['arn:load-balancer1', 'arn:load-balancer2'] }, @@ -79,6 +82,7 @@ describe('load balancer context provider plugin', () => { provider.getValue({ account: '1234', region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, loadBalancerTags: [ { key: 'some', value: 'tag' }, ], @@ -100,6 +104,7 @@ describe('load balancer context provider plugin', () => { CanonicalHostedZoneId: 'Z1234', SecurityGroups: ['sg-1234'], VpcId: 'vpc-1234', + Type: 'application', }, ], }); @@ -108,6 +113,7 @@ describe('load balancer context provider plugin', () => { const result = await provider.getValue({ account: '1234', region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, loadBalancerArn: 'arn:load-balancer1', }); @@ -133,6 +139,7 @@ describe('load balancer context provider plugin', () => { CanonicalHostedZoneId: 'Z1234', SecurityGroups: ['sg-1234'], VpcId: 'vpc-1234', + Type: 'application', }, { IpAddressType: 'ipv4', @@ -141,6 +148,7 @@ describe('load balancer context provider plugin', () => { CanonicalHostedZoneId: 'Z1234', SecurityGroups: ['sg-1234'], VpcId: 'vpc-1234', + Type: 'application', }, ], describeTagsExpected: { ResourceArns: ['arn:load-balancer1', 'arn:load-balancer2'] }, @@ -162,14 +170,17 @@ describe('load balancer context provider plugin', () => { }); // WHEN - await provider.getValue({ + const result = await provider.getValue({ account: '1234', region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, loadBalancerTags: [ { key: 'some', value: 'tag' }, { key: 'second', value: 'tag2' }, ], }); + + expect(result.loadBalancerArn).toEqual('arn:load-balancer2'); }); test('filters by type', async () => { @@ -197,12 +208,24 @@ describe('load balancer context provider plugin', () => { VpcId: 'vpc-1234', }, ], + + tagDescriptions: [ + { + ResourceArn: 'arn:load-balancer1', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + { + ResourceArn: 'arn:load-balancer2', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + ], }); // WHEN const loadBalancer = await provider.getValue({ account: '1234', region: 'us-east-1', + loadBalancerTags: [{ key: 'some', value: 'tag' }], loadBalancerType: cxschema.LoadBalancerType.APPLICATION, }); From b5a10af1ef9b6b78ec0627d2ff9f85f2fc6ba26c Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 13:44:19 -0600 Subject: [PATCH 31/51] fix: listener query should error when missing filters --- .../lib/context-providers/load-balancers.ts | 4 ++ .../context-providers/load-balancers.test.ts | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index cd12aebd2c2f8..21b3bbb7348a1 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -59,6 +59,10 @@ export class LoadBalancerListenerContextProviderPlugin implements ContextProvide async getValue(query: LoadBalancerListenerQuery): Promise { const elbv2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(query.account, query.region), Mode.ForReading)).elbv2(); + if (!query.listenerArn && !query.loadBalancerArn && !query.loadBalancerTags) { + throw new Error('The load balancer listener query must specify at least one of: `listenerArn`, `loadBalancerArn` or `loadBalancerTags`'); + } + if (query.listenerArn) { // When we know a listener arn, we can query for that listener directly. return this.getListenerByArn(elbv2, query); diff --git a/packages/aws-cdk/test/context-providers/load-balancers.test.ts b/packages/aws-cdk/test/context-providers/load-balancers.test.ts index ab276e2c3c469..1d20123d6ab8f 100644 --- a/packages/aws-cdk/test/context-providers/load-balancers.test.ts +++ b/packages/aws-cdk/test/context-providers/load-balancers.test.ts @@ -247,6 +247,8 @@ describe('load balancer listener context provider plugin', () => { provider.getValue({ account: '1234', region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerTags: [{ key: 'some', value: 'tag' }], }), ).rejects.toThrow(/No associated load balancers found/i); }); @@ -259,6 +261,7 @@ describe('load balancer listener context provider plugin', () => { loadBalancers: [ { LoadBalancerArn: 'arn:load-balancer', + Type: 'application', }, ], listeners: [ @@ -276,6 +279,8 @@ describe('load balancer listener context provider plugin', () => { provider.getValue({ account: '1234', region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerArn: 'arn:load-balancer', listenerPort: 443, listenerProtocol: cxschema.LoadBalancerListenerProtocol.HTTPS, }), @@ -290,9 +295,21 @@ describe('load balancer listener context provider plugin', () => { loadBalancers: [ { LoadBalancerArn: 'arn:load-balancer', + Type: 'application', }, { LoadBalancerArn: 'arn:load-balancer2', + Type: 'application', + }, + ], + tagDescriptions: [ + { + ResourceArn: 'arn:load-balancer', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + { + ResourceArn: 'arn:load-balancer2', + Tags: [{ Key: 'some', Value: 'tag' }], }, ], listeners: [ @@ -316,6 +333,8 @@ describe('load balancer listener context provider plugin', () => { provider.getValue({ account: '1234', region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerTags: [{ key: 'some', value: 'tag' }], listenerPort: 80, listenerProtocol: cxschema.LoadBalancerListenerProtocol.HTTP, }), @@ -340,6 +359,7 @@ describe('load balancer listener context provider plugin', () => { { LoadBalancerArn: 'arn:load-balancer-arn', SecurityGroups: ['sg-1234', 'sg-2345'], + Type: 'application', }, ], }); @@ -348,6 +368,7 @@ describe('load balancer listener context provider plugin', () => { const listener = await provider.getValue({ account: '1234', region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, listenerArn: 'arn:listener-arn', }); @@ -367,6 +388,7 @@ describe('load balancer listener context provider plugin', () => { { LoadBalancerArn: 'arn:load-balancer-arn1', SecurityGroups: ['sg-1234'], + Type: 'application', }, ], @@ -385,6 +407,7 @@ describe('load balancer listener context provider plugin', () => { const listener = await provider.getValue({ account: '1234', region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, loadBalancerArn: 'arn:load-balancer-arn1', }); @@ -405,11 +428,13 @@ describe('load balancer listener context provider plugin', () => { // This one should have the wrong tags LoadBalancerArn: 'arn:load-balancer-arn1', SecurityGroups: ['sg-1234', 'sg-2345'], + Type: 'application', }, { // Expecting this one LoadBalancerArn: 'arn:load-balancer-arn2', SecurityGroups: ['sg-3456', 'sg-4567'], + Type: 'application', }, ], @@ -448,6 +473,7 @@ describe('load balancer listener context provider plugin', () => { const listener = await provider.getValue({ account: '1234', region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, loadBalancerTags: [ { key: 'some', value: 'tag' }, ], @@ -476,6 +502,7 @@ describe('load balancer listener context provider plugin', () => { CanonicalHostedZoneId: 'Z1234', SecurityGroups: ['sg-1234'], VpcId: 'vpc-1234', + Type: 'application', }, { // Should have a matching listener @@ -485,6 +512,22 @@ describe('load balancer listener context provider plugin', () => { CanonicalHostedZoneId: 'Z1234', SecurityGroups: ['sg-2345'], VpcId: 'vpc-1234', + Type: 'application', + }, + ], + }); + }); + + AWS.mock('ELBv2', 'describeTags', (_params: aws.ELBv2.DescribeTagsInput, cb: AwsCallback) => { + cb(null, { + TagDescriptions: [ + { + ResourceArn: 'arn:load-balancer1', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + { + ResourceArn: 'arn:load-balancer2', + Tags: [{ Key: 'some', Value: 'tag' }], }, ], }); @@ -545,6 +588,8 @@ describe('load balancer listener context provider plugin', () => { const listener = await provider.getValue({ account: '1234', region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerTags: [{ key: 'some', value: 'tag' }], listenerProtocol: cxschema.LoadBalancerListenerProtocol.TCP, listenerPort: 443, }); @@ -576,6 +621,17 @@ describe('load balancer listener context provider plugin', () => { }, ], + tagDescriptions: [ + { + ResourceArn: 'arn:load-balancer-arn1', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + { + ResourceArn: 'arn:load-balancer-arn2', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + ], + describeListenersExpected: { LoadBalancerArn: 'arn:load-balancer-arn2' }, listeners: [ { @@ -591,6 +647,7 @@ describe('load balancer listener context provider plugin', () => { account: '1234', region: 'us-east-1', loadBalancerType: cxschema.LoadBalancerType.NETWORK, + loadBalancerTags: [{ key: 'some', value: 'tag' }], listenerPort: 443, }); From e3a935bb85fc96e13e53e4952e49572bb947bfa9 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 13:45:28 -0600 Subject: [PATCH 32/51] fix: unnecessary non-null assertion --- packages/aws-cdk/lib/context-providers/load-balancers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index 21b3bbb7348a1..47e0eee479dde 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -164,7 +164,7 @@ export class LoadBalancerListenerContextProviderPlugin implements ContextProvide async function findLoadBalancers(elbv2: AWS.ELBv2, args: cxschema.LoadBalancerFilter) { // List load balancers let loadBalancers = await describeLoadBalancers(elbv2, { - LoadBalancerArns: args.loadBalancerArn ? [args.loadBalancerArn!] : undefined, + LoadBalancerArns: args.loadBalancerArn ? [args.loadBalancerArn] : undefined, }); // Filter by load balancer type From 030f7543ced809227d6adc7f8dfd1795fd897581 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 13:47:05 -0600 Subject: [PATCH 33/51] refactor: simplify load balancer collection --- packages/aws-cdk/lib/context-providers/load-balancers.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index 47e0eee479dde..404e143abfa51 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -189,10 +189,8 @@ async function describeLoadBalancers(elbv2: AWS.ELBv2, request: AWS.ELBv2.Descri ...request, Marker: page ? page.NextMarker : undefined, }).promise(); - const pageItems = page.LoadBalancers ?? []; - for (const loadBalancer of pageItems) { - loadBalancers.push(loadBalancer); - } + + loadBalancers.push(...Array.from(page.LoadBalancers ?? [])); } while (page.NextMarker); return loadBalancers; From fa1bcc668468503822564cccf9d827e045c4c287 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 13:50:33 -0600 Subject: [PATCH 34/51] refactor: use optional chaining for next page markers --- packages/aws-cdk/lib/context-providers/load-balancers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index 404e143abfa51..09475c0202021 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -187,7 +187,7 @@ async function describeLoadBalancers(elbv2: AWS.ELBv2, request: AWS.ELBv2.Descri do { page = await elbv2.describeLoadBalancers({ ...request, - Marker: page ? page.NextMarker : undefined, + Marker: page?.NextMarker, }).promise(); loadBalancers.push(...Array.from(page.LoadBalancers ?? [])); @@ -265,7 +265,7 @@ async function* describeListenersByLoadBalancerArn(elbv2: AWS.ELBv2, loadBalance do { page = await elbv2.describeListeners({ LoadBalancerArn: loadBalancerArn, - Marker: page ? page.NextMarker : undefined, + Marker: page?.NextMarker, }).promise(); for (const listener of page.Listeners ?? []) { From 9555850102046bae2b41904eaf1ef7b806c8da44 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 14:04:04 -0600 Subject: [PATCH 35/51] fix: detect ipv6 egress --- .../lib/context-providers/security-groups.ts | 14 ++++++--- .../context-providers/security-groups.test.ts | 30 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/aws-cdk/lib/context-providers/security-groups.ts b/packages/aws-cdk/lib/context-providers/security-groups.ts index 4133f8a6b60bd..e8f464128b68d 100644 --- a/packages/aws-cdk/lib/context-providers/security-groups.ts +++ b/packages/aws-cdk/lib/context-providers/security-groups.ts @@ -36,14 +36,20 @@ export class SecurityGroupContextProviderPlugin implements ContextProviderPlugin * @internal */ export function hasAllTrafficEgress(securityGroup: AWS.EC2.SecurityGroup) { + let hasAllTrafficCidrV4 = false; + let hasAllTrafficCidrV6 = false; + for (const ipPermission of securityGroup.IpPermissionsEgress ?? []) { - const hasAllTrafficCidr = Boolean(ipPermission.IpRanges?.find(m => m.CidrIp === '0.0.0.0/0')); const isAllProtocols = ipPermission.IpProtocol === '-1'; - if (hasAllTrafficCidr && isAllProtocols) { - return true; + if (isAllProtocols && ipPermission.IpRanges?.some(m => m.CidrIp === '0.0.0.0/0')) { + hasAllTrafficCidrV4 = true; + } + + if (isAllProtocols && ipPermission.Ipv6Ranges?.some(m => m.CidrIpv6 === '::/0')) { + hasAllTrafficCidrV6 = true; } } - return false; + return hasAllTrafficCidrV4 && hasAllTrafficCidrV6; } diff --git a/packages/aws-cdk/test/context-providers/security-groups.test.ts b/packages/aws-cdk/test/context-providers/security-groups.test.ts index 1f391ba50c8df..7f24e684819b6 100644 --- a/packages/aws-cdk/test/context-providers/security-groups.test.ts +++ b/packages/aws-cdk/test/context-providers/security-groups.test.ts @@ -50,6 +50,12 @@ describe('security group context provider plugin', () => { { CidrIp: '0.0.0.0/0' }, ], }, + { + IpProtocol: '-1', + Ipv6Ranges: [ + { CidrIpv6: '::/0' }, + ], + }, ], }, ], @@ -113,6 +119,30 @@ describe('security group context provider plugin', () => { { CidrIp: '0.0.0.0/0' }, ], }, + { + IpProtocol: '-1', + Ipv6Ranges: [ + { CidrIpv6: '::/0' }, + ], + }, + ], + }), + ).toBe(true); + }); + + test('identifies allTrafficEgress from SecurityGroup permissions when combined', () => { + expect( + hasAllTrafficEgress({ + IpPermissionsEgress: [ + { + IpProtocol: '-1', + IpRanges: [ + { CidrIp: '0.0.0.0/0' }, + ], + Ipv6Ranges: [ + { CidrIpv6: '::/0' }, + ], + }, ], }), ).toBe(true); From 8cfbe3fe9ad682a4b85f8e74cc6f983950bf19b4 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 15:18:59 -0600 Subject: [PATCH 36/51] fix: does not handle undefined tag values --- .../lib/context-providers/load-balancers.ts | 7 +- .../context-providers/load-balancers.test.ts | 70 ++++++++++++++++++- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index 09475c0202021..27a2edb0e4434 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -237,11 +237,12 @@ async function* describeTags(elbv2: AWS.ELBv2, resourceArns: string[]) { /** * Determines if the given TagDescription matches the required tags. + * @internal */ -function tagsMatch(tagDescription: AWS.ELBv2.TagDescription, requiredTags: cxschema.Tag[]) { - const tagsByName: Record = {}; +export function tagsMatch(tagDescription: AWS.ELBv2.TagDescription, requiredTags: cxschema.Tag[]) { + const tagsByName: Record = {}; for (const tag of tagDescription.Tags ?? []) { - tagsByName[tag.Key!] = tag.Value!; + tagsByName[tag.Key!] = tag.Value; } for (const tag of requiredTags) { diff --git a/packages/aws-cdk/test/context-providers/load-balancers.test.ts b/packages/aws-cdk/test/context-providers/load-balancers.test.ts index 1d20123d6ab8f..147ffd54702de 100644 --- a/packages/aws-cdk/test/context-providers/load-balancers.test.ts +++ b/packages/aws-cdk/test/context-providers/load-balancers.test.ts @@ -1,7 +1,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as aws from 'aws-sdk'; import * as AWS from 'aws-sdk-mock'; -import { LoadBalancerListenerContextProviderPlugin, LoadBalancerContextProviderPlugin } from '../../lib/context-providers/load-balancers'; +import { LoadBalancerListenerContextProviderPlugin, LoadBalancerContextProviderPlugin, tagsMatch } from '../../lib/context-providers/load-balancers'; import { MockSdkProvider } from '../util/mock-sdk'; AWS.setSDK(require.resolve('aws-sdk')); @@ -15,6 +15,74 @@ afterEach(done => { done(); }); +describe('utilities', () => { + test('all tags match', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [{ Key: 'some', Value: 'tag' }], + }; + + const requiredTags = [ + { key: 'some', value: 'tag' }, + ]; + + expect(tagsMatch(tagDescription, requiredTags)).toEqual(true); + }); + + test('extra tags match', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [ + { Key: 'some', Value: 'tag' }, + { Key: 'other', Value: 'tag2' }, + ], + }; + + const requiredTags = [ + { key: 'some', value: 'tag' }, + ]; + + expect(tagsMatch(tagDescription, requiredTags)).toEqual(true); + }); + + test('no tags matches no tags', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [], + }; + + expect(tagsMatch(tagDescription, [])).toEqual(true); + }); + + test('one tag matches of several', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [{ Key: 'some', Value: 'tag' }], + }; + + const requiredTags = [ + { key: 'some', value: 'tag' }, + { key: 'other', value: 'value' }, + ]; + + expect(tagsMatch(tagDescription, requiredTags)).toEqual(false); + }); + + test('undefined tag does not error', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [{ Key: 'some' }], + }; + + const requiredTags = [ + { key: 'some', value: 'tag' }, + { key: 'other', value: 'value' }, + ]; + + expect(tagsMatch(tagDescription, requiredTags)).toEqual(false); + }); +}); + describe('load balancer context provider plugin', () => { test('errors when no matches are found', async () => { // GIVEN From 0a3a0a5bb01f68dcea63ab3bd213a2302e0cf3aa Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 16:00:26 -0600 Subject: [PATCH 37/51] chore: add tests for paginated calls --- .../lib/context-providers/load-balancers.ts | 9 +- .../context-providers/load-balancers.test.ts | 183 +++++++++++++----- packages/aws-cdk/test/util/mock-sdk.ts | 7 + 3 files changed, 148 insertions(+), 51 deletions(-) diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index 27a2edb0e4434..0e17a78ad8617 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -180,8 +180,9 @@ async function findLoadBalancers(elbv2: AWS.ELBv2, args: cxschema.LoadBalancerFi /** * Helper to paginate over describeLoadBalancers + * @internal */ -async function describeLoadBalancers(elbv2: AWS.ELBv2, request: AWS.ELBv2.DescribeLoadBalancersInput) { +export async function describeLoadBalancers(elbv2: AWS.ELBv2, request: AWS.ELBv2.DescribeLoadBalancersInput) { const loadBalancers = Array(); let page: AWS.ELBv2.DescribeLoadBalancersOutput | undefined; do { @@ -219,8 +220,9 @@ async function filterLoadBalancersByTags(elbv2: AWS.ELBv2, loadBalancers: AWS.EL * Generator function that yields `TagDescriptions`. The API doesn't support * pagination, so this generator breaks the resource list into chunks and issues * the appropriate requests, yielding each tag description as it receives it. + * @internal */ -async function* describeTags(elbv2: AWS.ELBv2, resourceArns: string[]) { +export async function* describeTags(elbv2: AWS.ELBv2, resourceArns: string[]) { // Max of 20 resource arns per request. const chunkSize = 20; for (let i = 0; i < resourceArns.length; i += chunkSize) { @@ -259,8 +261,9 @@ export function tagsMatch(tagDescription: AWS.ELBv2.TagDescription, requiredTags * pagination. Because describeListeners only lets you search by one load * balancer arn at a time, we request them individually and yield the listeners * as they come in. + * @internal */ -async function* describeListenersByLoadBalancerArn(elbv2: AWS.ELBv2, loadBalancerArns: string[]) { +export async function* describeListenersByLoadBalancerArn(elbv2: AWS.ELBv2, loadBalancerArns: string[]) { for (const loadBalancerArn of loadBalancerArns) { let page: AWS.ELBv2.DescribeListenersOutput | undefined; do { diff --git a/packages/aws-cdk/test/context-providers/load-balancers.test.ts b/packages/aws-cdk/test/context-providers/load-balancers.test.ts index 147ffd54702de..03ab1ac4cf989 100644 --- a/packages/aws-cdk/test/context-providers/load-balancers.test.ts +++ b/packages/aws-cdk/test/context-providers/load-balancers.test.ts @@ -1,7 +1,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as aws from 'aws-sdk'; import * as AWS from 'aws-sdk-mock'; -import { LoadBalancerListenerContextProviderPlugin, LoadBalancerContextProviderPlugin, tagsMatch } from '../../lib/context-providers/load-balancers'; +import { LoadBalancerListenerContextProviderPlugin, LoadBalancerContextProviderPlugin, tagsMatch, describeListenersByLoadBalancerArn, describeTags, describeLoadBalancers } from '../../lib/context-providers/load-balancers'; import { MockSdkProvider } from '../util/mock-sdk'; AWS.setSDK(require.resolve('aws-sdk')); @@ -16,70 +16,157 @@ afterEach(done => { }); describe('utilities', () => { - test('all tags match', () => { - const tagDescription = { - ResourceArn: 'arn:whatever', - Tags: [{ Key: 'some', Value: 'tag' }], + test('describeTags yields tags by chunk', async () => { + const resourceTags: Record = {}; + for (const resourceArn of [...Array(100)].map((_, i) => `arn:load-balancer-${i}`)) { + resourceTags[resourceArn] = { + ResourceArn: resourceArn, + Tags: [ + { Key: 'name', Value: resourceArn }, + ], + }; }; - const requiredTags = [ - { key: 'some', value: 'tag' }, - ]; + AWS.mock('ELBv2', 'describeTags', (_params: aws.ELBv2.DescribeTagsInput, cb: AwsCallback) => { + expect(_params.ResourceArns.length).toBeLessThanOrEqual(20); - expect(tagsMatch(tagDescription, requiredTags)).toEqual(true); - }); + cb(null, { + TagDescriptions: _params.ResourceArns.map(resourceArn => ({ + ResourceArn: resourceArn, + Tags: [ + { Key: 'name', Value: resourceArn }, + ], + })), + }); + }); - test('extra tags match', () => { - const tagDescription = { - ResourceArn: 'arn:whatever', - Tags: [ - { Key: 'some', Value: 'tag' }, - { Key: 'other', Value: 'tag2' }, - ], - }; + const elbv2 = await (await mockSDK.forEnvironment()).elbv2(); - const requiredTags = [ - { key: 'some', value: 'tag' }, - ]; + const resourceTagsOut: Record = {}; + for await (const tagDescription of describeTags(elbv2, Object.keys(resourceTags))) { + resourceTagsOut[tagDescription.ResourceArn!] = tagDescription; + } - expect(tagsMatch(tagDescription, requiredTags)).toEqual(true); + expect(resourceTagsOut).toEqual(resourceTags); }); - test('no tags matches no tags', () => { - const tagDescription = { - ResourceArn: 'arn:whatever', - Tags: [], - }; + test('describeListenersByLoadBalancerArn traverses pages', async () => { + // arn:listener-0, arn:listener-1, ..., arn:listener-99 + const listenerArns = [...Array(100)].map((_, i) => `arn:listener-${i}`); + expect(listenerArns[0]).toEqual('arn:listener-0'); + + AWS.mock('ELBv2', 'describeListeners', (_params: aws.ELBv2.DescribeListenersInput, cb: AwsCallback) => { + const start = parseInt(_params.Marker ?? '0'); + const end = start + 10; + const slice = listenerArns.slice(start, end); - expect(tagsMatch(tagDescription, [])).toEqual(true); + cb(null, { + Listeners: slice.map(arn => ({ + ListenerArn: arn, + })), + NextMarker: end < listenerArns.length ? end.toString() : undefined, + }); + }); + + const elbv2 = await (await mockSDK.forEnvironment()).elbv2(); + + const listenerArnsFromPages = Array(); + for await (const listener of describeListenersByLoadBalancerArn(elbv2, ['arn:load-balancer'])) { + listenerArnsFromPages.push(listener.ListenerArn!); + } + + expect(listenerArnsFromPages).toEqual(listenerArns); }); - test('one tag matches of several', () => { - const tagDescription = { - ResourceArn: 'arn:whatever', - Tags: [{ Key: 'some', Value: 'tag' }], - }; + test('describeLoadBalancers traverses pages', async () => { + const loadBalancerArns = [...Array(100)].map((_, i) => `arn:load-balancer-${i}`); + expect(loadBalancerArns[0]).toEqual('arn:load-balancer-0'); - const requiredTags = [ - { key: 'some', value: 'tag' }, - { key: 'other', value: 'value' }, - ]; + AWS.mock('ELBv2', 'describeLoadBalancers', (_params: aws.ELBv2.DescribeLoadBalancersInput, cb: AwsCallback) => { + const start = parseInt(_params.Marker ?? '0'); + const end = start + 10; + const slice = loadBalancerArns.slice(start, end); - expect(tagsMatch(tagDescription, requiredTags)).toEqual(false); + cb(null, { + LoadBalancers: slice.map(loadBalancerArn => ({ + LoadBalancerArn: loadBalancerArn, + })), + NextMarker: end < loadBalancerArns.length ? end.toString() : undefined, + }); + }); + + const elbv2 = await (await mockSDK.forEnvironment()).elbv2(); + const loadBalancerArnsFromPages = (await describeLoadBalancers(elbv2, {})).map(l => l.LoadBalancerArn!); + + expect(loadBalancerArnsFromPages).toEqual(loadBalancerArns); }); - test('undefined tag does not error', () => { - const tagDescription = { - ResourceArn: 'arn:whatever', - Tags: [{ Key: 'some' }], - }; + describe('tagsMatch', () => { + test('all tags match', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [{ Key: 'some', Value: 'tag' }], + }; + + const requiredTags = [ + { key: 'some', value: 'tag' }, + ]; + + expect(tagsMatch(tagDescription, requiredTags)).toEqual(true); + }); + + test('extra tags match', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [ + { Key: 'some', Value: 'tag' }, + { Key: 'other', Value: 'tag2' }, + ], + }; + + const requiredTags = [ + { key: 'some', value: 'tag' }, + ]; + + expect(tagsMatch(tagDescription, requiredTags)).toEqual(true); + }); + + test('no tags matches no tags', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [], + }; + + expect(tagsMatch(tagDescription, [])).toEqual(true); + }); + + test('one tag matches of several', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [{ Key: 'some', Value: 'tag' }], + }; + + const requiredTags = [ + { key: 'some', value: 'tag' }, + { key: 'other', value: 'value' }, + ]; + + expect(tagsMatch(tagDescription, requiredTags)).toEqual(false); + }); - const requiredTags = [ - { key: 'some', value: 'tag' }, - { key: 'other', value: 'value' }, - ]; + test('undefined tag does not error', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [{ Key: 'some' }], + }; - expect(tagsMatch(tagDescription, requiredTags)).toEqual(false); + const requiredTags = [ + { key: 'some', value: 'tag' }, + { key: 'other', value: 'value' }, + ]; + + expect(tagsMatch(tagDescription, requiredTags)).toEqual(false); + }); }); }); diff --git a/packages/aws-cdk/test/util/mock-sdk.ts b/packages/aws-cdk/test/util/mock-sdk.ts index c3ba69bb37111..28ddf279620ef 100644 --- a/packages/aws-cdk/test/util/mock-sdk.ts +++ b/packages/aws-cdk/test/util/mock-sdk.ts @@ -77,6 +77,13 @@ export class MockSdkProvider extends SdkProvider { public stubSTS(stubs: SyncHandlerSubsetOf) { (this.sdk as any).sts = jest.fn().mockReturnValue(partialAwsService(stubs)); } + + /** + * Replace the STS client with the given object + */ + public stubELBv2(stubs: SyncHandlerSubsetOf) { + (this.sdk as any).elbv2 = jest.fn().mockReturnValue(partialAwsService(stubs)); + } } export class MockSdk implements ISDK { From 5dfc521c93a03803f1b79ee196a47a2451016c3f Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 16:24:57 -0600 Subject: [PATCH 38/51] fix: restore the vpc-like user tag input --- .../lib/shared/base-listener.ts | 17 +++++++++++++---- .../lib/shared/base-load-balancer.ts | 14 ++++++++++---- .../lib/shared/util.ts | 12 +++++++++++- .../test/alb/listener.test.ts | 18 +++++++++--------- .../test/alb/load-balancer.test.ts | 12 ++++++------ .../test/nlb/listener.test.ts | 6 +++--- .../test/nlb/load-balancer.test.ts | 12 ++++++------ 7 files changed, 58 insertions(+), 33 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index 2cf7211184921..c4f03d2b8bcf0 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -4,6 +4,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { CfnListener } from '../elasticloadbalancingv2.generated'; import { IListenerAction } from './listener-action'; +import { mapTagMapToCxschema } from './util'; /** * Options for listener lookup @@ -25,7 +26,7 @@ export interface BaseListenerLookupOptions { * Filter listeners by associated load balancer tags * @default - does not filter by load balancer tags */ - readonly loadBalancerTags?: cxschema.Tag[]; + readonly loadBalancerTags?: Record; /** * Filter listeners by listener port @@ -66,18 +67,26 @@ export abstract class BaseListener extends Resource { */ protected static _queryContextProvider(scope: Construct, options: ListenerQueryContextProviderOptions) { if (Token.isUnresolved(options.userOptions.loadBalancerArn) - || Token.isUnresolved(options.userOptions.loadBalancerTags) + || Object.values(options.userOptions.loadBalancerTags ?? {}).some(Token.isUnresolved) || Token.isUnresolved(options.userOptions.listenerArn) || Token.isUnresolved(options.userOptions.listenerPort)) { throw new Error('All arguments to look up a load balancer listener must be concrete (no Tokens)'); } + let cxschemaTags: cxschema.Tag[] | undefined; + if (options.userOptions.loadBalancerTags) { + cxschemaTags = mapTagMapToCxschema(options.userOptions.loadBalancerTags); + } + const props: cxapi.LoadBalancerListenerContextResponse = ContextProvider.getValue(scope, { provider: cxschema.ContextProvider.LOAD_BALANCER_LISTENER_PROVIDER, props: { - ...options.userOptions, - loadBalancerType: options.loadBalancerType, + listenerArn: options.userOptions.listenerArn, + listenerPort: options.userOptions.listenerPort, listenerProtocol: options.listenerProtocol, + loadBalancerArn: options.userOptions.loadBalancerArn, + loadBalancerTags: cxschemaTags, + loadBalancerType: options.loadBalancerType, } as cxschema.LoadBalancerListenerContextQuery, dummyValue: { listenerArn: `arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/${options.loadBalancerType}/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2`, diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts index 03d975e787820..46526b9376f68 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts @@ -7,7 +7,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import { RegionInfo } from '@aws-cdk/region-info'; import { Construct } from 'constructs'; import { CfnLoadBalancer } from '../elasticloadbalancingv2.generated'; -import { Attributes, ifUndefined, renderAttributes } from './util'; +import { Attributes, ifUndefined, mapTagMapToCxschema, renderAttributes } from './util'; /** * Shared properties of both Application and Network Load Balancers @@ -80,7 +80,7 @@ export interface BaseLoadBalancerLookupOptions { * Match load balancer tags. * @default - does not match load balancers by tags */ - readonly loadBalancerTags?: cxschema.Tag[]; + readonly loadBalancerTags?: Record; } /** @@ -109,14 +109,20 @@ export abstract class BaseLoadBalancer extends Resource { */ protected static _queryContextProvider(scope: Construct, options: LoadBalancerQueryContextProviderOptions) { if (Token.isUnresolved(options.userOptions.loadBalancerArn) - || Token.isUnresolved(options.userOptions.loadBalancerTags)) { + || Object.values(options.userOptions.loadBalancerTags ?? {}).some(Token.isUnresolved)) { throw new Error('All arguments to look up a load balancer must be concrete (no Tokens)'); } + let cxschemaTags: cxschema.Tag[] | undefined; + if (options.userOptions.loadBalancerTags) { + cxschemaTags = mapTagMapToCxschema(options.userOptions.loadBalancerTags); + } + const props: cxapi.LoadBalancerContextResponse = ContextProvider.getValue(scope, { provider: cxschema.ContextProvider.LOAD_BALANCER_PROVIDER, props: { - ...options.userOptions, + loadBalancerArn: options.userOptions.loadBalancerArn, + loadBalancerTags: cxschemaTags, loadBalancerType: options.loadBalancerType, } as cxschema.LoadBalancerContextQuery, dummyValue: { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts index 2776d533faefa..2ecc35c365694 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts @@ -1,3 +1,4 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { ApplicationProtocol, Protocol } from './enums'; export type Attributes = {[key: string]: string | undefined}; @@ -79,4 +80,13 @@ export function validateNetworkProtocol(protocol: Protocol) { if (NLB_PROTOCOLS.indexOf(protocol) === -1) { throw new Error(`The protocol must be one of ${NLB_PROTOCOLS.join(', ')}. Found ${protocol}`); } -} \ No newline at end of file +} + +/** + * Helper to map a map of tags to cxschema tag format. + * @internal + */ +export function mapTagMapToCxschema(tagMap: Record): cxschema.Tag[] { + return Object.entries(tagMap) + .map(([key, value]) => ({ key, value })); +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts index 23e0e15186874..b9de0961423ec 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -1378,9 +1378,9 @@ describe('tests', () => { // WHEN const listener = elbv2.ApplicationListener.fromLookup(stack, 'a', { - loadBalancerTags: [ - { key: 'some', value: 'tag' }, - ], + loadBalancerTags: { + some: 'tag', + }, }); // THEN @@ -1400,9 +1400,9 @@ describe('tests', () => { }); const listener = elbv2.ApplicationListener.fromLookup(stack, 'a', { - loadBalancerTags: [ - { key: 'some', value: 'tag' }, - ], + loadBalancerTags: { + some: 'tag', + }, }); // WHEN @@ -1432,9 +1432,9 @@ describe('tests', () => { }); const listener = elbv2.ApplicationListener.fromLookup(stack, 'a', { - loadBalancerTags: [ - { key: 'some', value: 'tag' }, - ], + loadBalancerTags: { + some: 'tag', + }, }); // WHEN diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts index 5f1dd1b0aeb0c..e2745a2fb02aa 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts @@ -348,9 +348,9 @@ describe('tests', () => { // WHEN const loadBalancer = elbv2.ApplicationLoadBalancer.fromLookup(stack, 'a', { - loadBalancerTags: [ - { key: 'some', value: 'tag' }, - ], + loadBalancerTags: { + some: 'tag', + }, }); // THEN @@ -373,9 +373,9 @@ describe('tests', () => { }); const loadBalancer = elbv2.ApplicationLoadBalancer.fromLookup(stack, 'a', { - loadBalancerTags: [ - { key: 'some', value: 'tag' }, - ], + loadBalancerTags: { + some: 'tag', + }, }); // WHEN diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts index e3042eec5bbb9..d40197d00a280 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts @@ -401,9 +401,9 @@ describe('tests', () => { // WHEN const listener = elbv2.NetworkListener.fromLookup(stack, 'a', { - loadBalancerTags: [ - { key: 'some', value: 'tag' }, - ], + loadBalancerTags: { + some: 'tag', + }, }); // THEN diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts index f0b5c3cbf80aa..88a3999ec0a6f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts @@ -415,9 +415,9 @@ describe('tests', () => { // WHEN const loadBalancer = elbv2.NetworkLoadBalancer.fromLookup(stack, 'a', { - loadBalancerTags: [ - { key: 'some', value: 'tag' }, - ], + loadBalancerTags: { + some: 'tag', + }, }); // THEN @@ -438,9 +438,9 @@ describe('tests', () => { }); const loadBalancer = elbv2.NetworkLoadBalancer.fromLookup(stack, 'a', { - loadBalancerTags: [ - { key: 'some', value: 'tag' }, - ], + loadBalancerTags: { + some: 'tag', + }, }); const targetGroup = new elbv2.NetworkTargetGroup(stack, 'tg', { From 3664f0054b0817c6d455c2c628f91a8df47f1510 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 17:20:22 -0600 Subject: [PATCH 39/51] fix: stubELBv2 comment --- packages/aws-cdk/test/util/mock-sdk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/test/util/mock-sdk.ts b/packages/aws-cdk/test/util/mock-sdk.ts index 28ddf279620ef..a060cef470a86 100644 --- a/packages/aws-cdk/test/util/mock-sdk.ts +++ b/packages/aws-cdk/test/util/mock-sdk.ts @@ -79,7 +79,7 @@ export class MockSdkProvider extends SdkProvider { } /** - * Replace the STS client with the given object + * Replace the ELBv2 client with the given object */ public stubELBv2(stubs: SyncHandlerSubsetOf) { (this.sdk as any).elbv2 = jest.fn().mockReturnValue(partialAwsService(stubs)); From c4e9973c8e534677cf5dcbd3e857b686a1754103 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 28 Oct 2020 17:29:58 -0600 Subject: [PATCH 40/51] docs: note the error from multiple matches --- .../@aws-cdk/aws-elasticloadbalancingv2/README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index fb89dc80fcfdf..49cfd5dc5db81 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -375,7 +375,9 @@ following lookup methods: You may look up a load balancer by ARN or by associated tags. When you look a load balancer up by ARN, that load balancer will be returned unless CDK detects that the load balancer is of the wrong type. When you look up a load balancer by -tags, CDK will return the first load balancer matching all specified tags. +tags, CDK will return the load balancer matching all specified tags. If more +than one load balancer matches, CDK will throw an error requesting that you +provide more specific criteria. **Look up a Application Load Balancer by ARN** ```ts @@ -399,12 +401,15 @@ const loadBalancer = ApplicationLoadBalancer.fromLookup(stack, 'ALB', { You may look up a load balancer listener by the following criteria: -- Associated load balancer arn +- Associated load balancer ARN - Associated load balancer tags +- Listener ARN - Listener port - Listener protocol -The lookup method will return the first matching listener rule. +The lookup method will return the matching listener. If more than one listener +matches, CDK will throw an error requesting that you specify additional +criteria. **Look up a Listener by associated Load Balancer, Port, and Protocol** From 0acf72bfcbe9b2d0450a0d5f19570850498e716a Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 29 Oct 2020 13:34:35 -0600 Subject: [PATCH 41/51] fix: allowed breaking change --- allowed-breaking-changes.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index 9716b21d44fe2..b216e48a3efd6 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -54,5 +54,5 @@ incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition.addVolume # | LoadBalancerContextQuery # | LoadBalancerListenerContextQuery # | SecurityGroupContextQuery -# These additions changed the signature of loadAssemblyManifest() -change-return-type:@aws-cdk/cloud-assembly-schema.Manifest.loadAssemblyManifest +# These additions changed the signature of MissingContext +weakened:@aws-cdk/cloud-assembly-schema.MissingContext From e2a133a2221758a3c3d97db8a44f02b866760d2b Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 29 Oct 2020 13:35:24 -0600 Subject: [PATCH 42/51] fix: missing public modifier --- .../aws-elasticloadbalancingv2/lib/alb/application-listener.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 3e41de3e9bd5f..a8f305ebe1694 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -128,7 +128,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis /** * Look up an ApplicationListener. */ - static fromLookup(scope: Construct, id: string, options: ApplicationListenerLookupOptions): IApplicationListener { + public static fromLookup(scope: Construct, id: string, options: ApplicationListenerLookupOptions): IApplicationListener { const props = BaseListener._queryContextProvider(scope, { userOptions: options, loadBalancerType: cxschema.LoadBalancerType.APPLICATION, From cbfc36b172408708f17e64e3d6c79bbc654575cb Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 29 Oct 2020 13:40:13 -0600 Subject: [PATCH 43/51] fix: unable to allow optional listener protocol --- .../lib/alb/application-listener.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index a8f305ebe1694..1f6d3bf2f48f7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -129,12 +129,16 @@ export class ApplicationListener extends BaseListener implements IApplicationLis * Look up an ApplicationListener. */ public static fromLookup(scope: Construct, id: string, options: ApplicationListenerLookupOptions): IApplicationListener { + let listenerProtocol: cxschema.LoadBalancerListenerProtocol | undefined; + switch (options.listenerProtocol) { + case ApplicationProtocol.HTTP: listenerProtocol = cxschema.LoadBalancerListenerProtocol.HTTP; break; + case ApplicationProtocol.HTTPS: listenerProtocol = cxschema.LoadBalancerListenerProtocol.HTTPS; break; + } + const props = BaseListener._queryContextProvider(scope, { userOptions: options, loadBalancerType: cxschema.LoadBalancerType.APPLICATION, - listenerProtocol: options.listenerProtocol === ApplicationProtocol.HTTP - ? cxschema.LoadBalancerListenerProtocol.HTTP - : cxschema.LoadBalancerListenerProtocol.HTTPS, + listenerProtocol, }); return new LookedUpApplicationListener(scope, id, props); From 744742485561d7c564be6b141077ec3a05f84409 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 29 Oct 2020 13:42:43 -0600 Subject: [PATCH 44/51] refactor: rename ImportedApplicationListenerBase -> ExternalApplicationListener --- .../lib/alb/application-listener.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 1f6d3bf2f48f7..dae43569e722e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -549,7 +549,7 @@ export interface ApplicationListenerAttributes { readonly securityGroupAllowsAllOutbound?: boolean; } -abstract class ImportedApplicationListenerBase extends Resource implements IApplicationListener { +abstract class ExternalApplicationListener extends Resource implements IApplicationListener { /** * Connections object. */ @@ -628,7 +628,7 @@ abstract class ImportedApplicationListenerBase extends Resource implements IAppl /** * An imported application listener. */ -class ImportedApplicationListener extends ImportedApplicationListenerBase { +class ImportedApplicationListener extends ExternalApplicationListener { public readonly listenerArn: string; public readonly connections: ec2.Connections; @@ -656,7 +656,7 @@ class ImportedApplicationListener extends ImportedApplicationListenerBase { } } -class LookedUpApplicationListener extends ImportedApplicationListenerBase { +class LookedUpApplicationListener extends ExternalApplicationListener { public readonly listenerArn: string; public readonly connections: ec2.Connections; From 85a20912f263136b01e6a7c29bcbd4722b48c415 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 29 Oct 2020 13:48:07 -0600 Subject: [PATCH 45/51] refactor: move listenerArn to ApplicationListenerLookupOptions --- .../lib/alb/application-listener.ts | 11 +++++++++++ .../lib/shared/base-listener.ts | 15 +++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index dae43569e722e..3a055b5fce4be 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -112,6 +112,12 @@ export interface ApplicationListenerProps extends BaseApplicationListenerProps { * Options for ApplicationListener lookup */ export interface ApplicationListenerLookupOptions extends BaseListenerLookupOptions { + /** + * ARN of the listener to look up + * @default - does not filter by listener arn + */ + readonly listenerArn?: string; + /** * Filter listeners by listener protocol * @default - does not filter by listener protocol @@ -129,6 +135,10 @@ export class ApplicationListener extends BaseListener implements IApplicationLis * Look up an ApplicationListener. */ public static fromLookup(scope: Construct, id: string, options: ApplicationListenerLookupOptions): IApplicationListener { + if (Token.isUnresolved(options.listenerArn)) { + throw new Error('All arguments to look up a load balancer listener must be concrete (no Tokens)'); + } + let listenerProtocol: cxschema.LoadBalancerListenerProtocol | undefined; switch (options.listenerProtocol) { case ApplicationProtocol.HTTP: listenerProtocol = cxschema.LoadBalancerListenerProtocol.HTTP; break; @@ -138,6 +148,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis const props = BaseListener._queryContextProvider(scope, { userOptions: options, loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + listenerArn: options.listenerArn, listenerProtocol, }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index c4f03d2b8bcf0..b735d6e375870 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -10,12 +10,6 @@ import { mapTagMapToCxschema } from './util'; * Options for listener lookup */ export interface BaseListenerLookupOptions { - /** - * ARN of the listener to look up - * @default - does not filter by listener arn - */ - readonly listenerArn?: string; - /** * Filter listeners by associated load balancer arn * @default - does not filter by load balancer arn @@ -50,6 +44,12 @@ export interface ListenerQueryContextProviderOptions { */ readonly loadBalancerType: cxschema.LoadBalancerType; + /** + * ARN of the listener to look up + * @default - does not filter by listener arn + */ + readonly listenerArn?: string; + /** * Optional protocol of the listener to look up */ @@ -68,7 +68,6 @@ export abstract class BaseListener extends Resource { protected static _queryContextProvider(scope: Construct, options: ListenerQueryContextProviderOptions) { if (Token.isUnresolved(options.userOptions.loadBalancerArn) || Object.values(options.userOptions.loadBalancerTags ?? {}).some(Token.isUnresolved) - || Token.isUnresolved(options.userOptions.listenerArn) || Token.isUnresolved(options.userOptions.listenerPort)) { throw new Error('All arguments to look up a load balancer listener must be concrete (no Tokens)'); } @@ -81,7 +80,7 @@ export abstract class BaseListener extends Resource { const props: cxapi.LoadBalancerListenerContextResponse = ContextProvider.getValue(scope, { provider: cxschema.ContextProvider.LOAD_BALANCER_LISTENER_PROVIDER, props: { - listenerArn: options.userOptions.listenerArn, + listenerArn: options.listenerArn, listenerPort: options.userOptions.listenerPort, listenerProtocol: options.listenerProtocol, loadBalancerArn: options.userOptions.loadBalancerArn, From 8928287216a18b3b1d97821912ba572f04acd536 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 29 Oct 2020 13:51:20 -0600 Subject: [PATCH 46/51] docs: improve description of allowAllOutbound --- packages/@aws-cdk/cx-api/lib/context/security-group.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/cx-api/lib/context/security-group.ts b/packages/@aws-cdk/cx-api/lib/context/security-group.ts index 3cf59db485b34..f1ea8c2a7ca37 100644 --- a/packages/@aws-cdk/cx-api/lib/context/security-group.ts +++ b/packages/@aws-cdk/cx-api/lib/context/security-group.ts @@ -9,8 +9,9 @@ export interface SecurityGroupContextResponse { readonly securityGroupId: string; /** - * Whether the security group of the load balancer allows all outbound - * traffic. + * Whether the security group allows all outbound traffic. This will be true + * when the security group has all-protocol egress permissions to access both + * `0.0.0.0/0` and `::/0`. */ readonly allowAllOutbound: boolean; } From 6a4e369e527b220596e29ae55421969a900b246f Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 29 Oct 2020 14:30:41 -0600 Subject: [PATCH 47/51] fix: public accessor missing from SecurityGroup.fromLookup --- packages/@aws-cdk/aws-ec2/lib/security-group.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index 33d6666a0ef9d..5ff6cf50c8419 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -299,7 +299,7 @@ export class SecurityGroup extends SecurityGroupBase { /** * Look up a security group by id. */ - static fromLookup(scope: Construct, id: string, securityGroupId: string) { + public static fromLookup(scope: Construct, id: string, securityGroupId: string) { if (Token.isUnresolved(securityGroupId)) { throw new Error('All arguments to look up a security group must be concrete (no Tokens)'); } From f4c332ad16c5f1192166b43eaafbe1a0934803fe Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 29 Oct 2020 15:05:43 -0600 Subject: [PATCH 48/51] fix: remove out of date comment --- packages/aws-cdk/lib/context-providers/load-balancers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index 0e17a78ad8617..900ba1e67841d 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -119,7 +119,6 @@ export class LoadBalancerListenerContextProviderPlugin implements ContextProvide throw new Error(`No associated load balancers found for load balancer listener query ${JSON.stringify(args)}`); } - // Find the first matching listener return this.findMatchingListener(elbv2, loadBalancers, args); } From f0ecfcda99143038dbd4263ab8b5f283974261f5 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 5 Nov 2020 09:19:05 -0700 Subject: [PATCH 49/51] fix: missing public modifier --- .../lib/alb/application-load-balancer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index 4f869413b968f..0b6c9d76a7d8f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -650,7 +650,7 @@ class LookedUpApplicationLoadBalancer extends Resource implements IApplicationLo } } - addListener(id: string, props: BaseApplicationListenerProps): ApplicationListener { + public addListener(id: string, props: BaseApplicationListenerProps): ApplicationListener { return new ApplicationListener(this, id, { ...props, loadBalancer: this, From 004497450763d562966c7d6dc43c5dcfa42c576a Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 5 Nov 2020 09:37:14 -0700 Subject: [PATCH 50/51] style: use ternary for if else --- packages/aws-cdk/lib/context-providers/load-balancers.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index 900ba1e67841d..26f00c7746fe3 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -63,14 +63,7 @@ export class LoadBalancerListenerContextProviderPlugin implements ContextProvide throw new Error('The load balancer listener query must specify at least one of: `listenerArn`, `loadBalancerArn` or `loadBalancerTags`'); } - if (query.listenerArn) { - // When we know a listener arn, we can query for that listener directly. - return this.getListenerByArn(elbv2, query); - } else { - // When we don't know a listener arn, we need to find it by traversing - // all associated load balancers. - return this.getListenerByFilteringLoadBalancers(elbv2, query); - } + return query.listenerArn ? this.getListenerByArn(elbv2, query) : this.getListenerByFilteringLoadBalancers(elbv2, query); } /** From 2f2a992f52607d7939039b2ab6ad785ff937db6c Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 5 Nov 2020 09:39:55 -0700 Subject: [PATCH 51/51] chore: relocate allowed breaking change per review --- allowed-breaking-changes.txt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index b216e48a3efd6..9120903b01912 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -3,6 +3,10 @@ # and that won't typecheck if Manifest.load() adds a union arm and now returns A | B | C. change-return-type:@aws-cdk/cloud-assembly-schema.Manifest.load +# Adding any new context queries will add to the ContextQueryProperties type, +# which changes the signature of MissingContext. +weakened:@aws-cdk/cloud-assembly-schema.MissingContext + removed:@aws-cdk/core.BootstraplessSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN removed:@aws-cdk/core.DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN removed:@aws-cdk/core.DefaultStackSynthesizerProps.assetPublishingExternalId @@ -48,11 +52,3 @@ incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition. incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition.addVolume incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition. incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition.addVolume - -# The new ELBv2 context queries added these arms to the ContextQueryProperties -# union: -# | LoadBalancerContextQuery -# | LoadBalancerListenerContextQuery -# | SecurityGroupContextQuery -# These additions changed the signature of MissingContext -weakened:@aws-cdk/cloud-assembly-schema.MissingContext