From 7ed9cd14aa5aaff90badb6438a0941fbca2d370c Mon Sep 17 00:00:00 2001 From: Neil Baillie Date: Thu, 27 Oct 2022 09:26:26 +0100 Subject: [PATCH] feat(ec2): Vpc supports allocating CIDR from AWS IPAM (#22458) Allows Vpc to Use [Aws IPAM](https://docs.aws.amazon.com/vpc/latest/ipam/what-it-is-ipam.html) for Ip address assignment: ```ts import { IpAddresses } from '@aws-cdk/aws-ec2'; declare const pool: ec2.CfnIPAMPool; new ec2.Vpc(stack, 'TheVPC', { ipAddresses: ec2.IpAddresses.awsIpamAllocation({ ipv4IpamPoolId: pool.ref, ipv4NetmaskLength: 18, defaultSubnetIpv4NetmaskLength: 24 }) }); ``` This is useful for enterprise users that wish to adopt the benefits of centralised IP address management. It introduces `ipAddresses` property to allow the new configuration. ---- Thanks to @rix0rrr for support on this. --- closes #21333 ---- #22443 - Issue adds a fix to allow the clean up of the AWS Ipam resource used in ingeg-test testing. Would be better to implement something like this later. for now disclaimer added to integ-test clean up needed on Ipam. ---- ### All Submissions: * [X] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### New Features * [X] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-apprunner/README.md | 2 +- .../test/integ.service-vpc-connector.ts | 2 +- .../aws-apprunner/test/service.test.ts | 2 +- .../aws-apprunner/test/vpc-connector.test.ts | 8 +- packages/@aws-cdk/aws-ec2/README.md | 72 +- packages/@aws-cdk/aws-ec2/lib/cidr-splits.ts | 77 ++ packages/@aws-cdk/aws-ec2/lib/index.ts | 1 + packages/@aws-cdk/aws-ec2/lib/ip-addresses.ts | 312 ++++++++ packages/@aws-cdk/aws-ec2/lib/vpc.ts | 84 ++- .../@aws-cdk/aws-ec2/test/cidr-splits.test.ts | 39 + .../test/integ.reserved-private-subnet.ts | 2 +- .../@aws-cdk/aws-ec2/test/integ.vpc-ipam.ts | 83 +++ .../test/integ.vpn-pre-shared-key-token.ts | 2 +- .../aws-ec2/test/ip-addresses.test.ts | 411 +++++++++++ ...efaultTestDeployAssertB1CA1C3A.assets.json | 32 + ...aultTestDeployAssertB1CA1C3A.template.json | 134 ++++ .../index.js | 669 ++++++++++++++++++ .../aws-cdk-ec2-ipam-vpc.assets.json | 19 + .../aws-cdk-ec2-ipam-vpc.template.json | 261 +++++++ .../test/vpc-ipam.integ.snapshot/cdk.out | 1 + .../test/vpc-ipam.integ.snapshot/integ.json | 15 + .../vpc-ipam.integ.snapshot/manifest.json | 190 +++++ .../test/vpc-ipam.integ.snapshot/tree.json | 499 +++++++++++++ packages/@aws-cdk/aws-ec2/test/vpc.test.ts | 38 +- 24 files changed, 2893 insertions(+), 62 deletions(-) create mode 100644 packages/@aws-cdk/aws-ec2/lib/cidr-splits.ts create mode 100644 packages/@aws-cdk/aws-ec2/lib/ip-addresses.ts create mode 100644 packages/@aws-cdk/aws-ec2/test/cidr-splits.test.ts create mode 100644 packages/@aws-cdk/aws-ec2/test/integ.vpc-ipam.ts create mode 100644 packages/@aws-cdk/aws-ec2/test/ip-addresses.test.ts create mode 100644 packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/VpcIpamDefaultTestDeployAssertB1CA1C3A.assets.json create mode 100644 packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/VpcIpamDefaultTestDeployAssertB1CA1C3A.template.json create mode 100644 packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/asset.d47f7e6772bfdf47ecbc070ffe204baf53bacbfbf7814eb407bd8ea108c1c1bb.bundle/index.js create mode 100644 packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/aws-cdk-ec2-ipam-vpc.assets.json create mode 100644 packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/aws-cdk-ec2-ipam-vpc.template.json create mode 100644 packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/cdk.out create mode 100644 packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/integ.json create mode 100644 packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/manifest.json create mode 100644 packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/tree.json diff --git a/packages/@aws-cdk/aws-apprunner/README.md b/packages/@aws-cdk/aws-apprunner/README.md index 5e1c1c2b7f7e8..de5aa782b26df 100644 --- a/packages/@aws-cdk/aws-apprunner/README.md +++ b/packages/@aws-cdk/aws-apprunner/README.md @@ -143,7 +143,7 @@ To associate an App Runner service with a custom VPC, define `vpcConnector` for import * as ec2 from '@aws-cdk/aws-ec2'; const vpc = new ec2.Vpc(this, 'Vpc', { - cidr: '10.0.0.0/16', + ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16') }); const vpcConnector = new apprunner.VpcConnector(this, 'VpcConnector', { diff --git a/packages/@aws-cdk/aws-apprunner/test/integ.service-vpc-connector.ts b/packages/@aws-cdk/aws-apprunner/test/integ.service-vpc-connector.ts index 5a3aa23eb8008..487a1b2569f14 100644 --- a/packages/@aws-cdk/aws-apprunner/test/integ.service-vpc-connector.ts +++ b/packages/@aws-cdk/aws-apprunner/test/integ.service-vpc-connector.ts @@ -9,7 +9,7 @@ const stack = new cdk.Stack(app, 'integ-apprunner'); // Scenario 6: Create the service from ECR public with a VPC Connector const vpc = new ec2.Vpc(stack, 'Vpc', { - cidr: '10.0.0.0/16', + ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'), }); const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc }); diff --git a/packages/@aws-cdk/aws-apprunner/test/service.test.ts b/packages/@aws-cdk/aws-apprunner/test/service.test.ts index 6b1d882bcda27..ed41689c9f255 100644 --- a/packages/@aws-cdk/aws-apprunner/test/service.test.ts +++ b/packages/@aws-cdk/aws-apprunner/test/service.test.ts @@ -619,7 +619,7 @@ test('specifying a vpcConnector should assign the service to it and set the egre const stack = new cdk.Stack(app, 'demo-stack'); const vpc = new ec2.Vpc(stack, 'Vpc', { - cidr: '10.0.0.0/16', + ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'), }); const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc }); diff --git a/packages/@aws-cdk/aws-apprunner/test/vpc-connector.test.ts b/packages/@aws-cdk/aws-apprunner/test/vpc-connector.test.ts index 7eef67f924381..71baeac75b692 100644 --- a/packages/@aws-cdk/aws-apprunner/test/vpc-connector.test.ts +++ b/packages/@aws-cdk/aws-apprunner/test/vpc-connector.test.ts @@ -9,7 +9,7 @@ test('create a vpcConnector with all properties', () => { const stack = new cdk.Stack(app, 'demo-stack'); const vpc = new ec2.Vpc(stack, 'Vpc', { - cidr: '10.0.0.0/16', + ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'), }); const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc }); @@ -48,7 +48,7 @@ test('create a vpcConnector without a name', () => { const stack = new cdk.Stack(app, 'demo-stack'); const vpc = new ec2.Vpc(stack, 'Vpc', { - cidr: '10.0.0.0/16', + ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'), }); const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc }); @@ -85,7 +85,7 @@ test('create a vpcConnector without a security group should create one', () => { const stack = new cdk.Stack(app, 'demo-stack'); const vpc = new ec2.Vpc(stack, 'Vpc', { - cidr: '10.0.0.0/16', + ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'), }); // WHEN @@ -120,7 +120,7 @@ test('create a vpcConnector with an empty security group array should create one const stack = new cdk.Stack(app, 'demo-stack'); const vpc = new ec2.Vpc(stack, 'Vpc', { - cidr: '10.0.0.0/16', + ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'), }); // WHEN diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 9b24604d546ab..43048983fd705 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -217,6 +217,62 @@ new ec2.Vpc(this, 'TheVPC', { provider.connections.allowFrom(ec2.Peer.ipv4('1.2.3.4/8'), ec2.Port.tcp(80)); ``` +### Ip Address Management + +The VPC spans a supernet IP range, which contains the non-overlapping IPs of its contained subnets. Possible sources for this IP range are: + +* You specify an IP range directly by specifying a CIDR +* You allocate an IP range of a given size automatically from AWS IPAM + +By default the Vpc will allocate the `10.0.0.0/16` address range which will be exhaustively spread across all subnets in the subnet configuration. This behavior can be changed by passing an object that implements `IIpAddresses` to the `ipAddress` property of a Vpc. See the subsequent sections for the options. + +Be aware that if you don't explicitly reserve subnet groups in `subnetConfiguration`, the address space will be fully allocated! If you predict you may need to add more subnet groups later, add them early on and set `reserved: true` (see the "Advanced Subnet Configuration" section for more information). + +#### Specifying a CIDR directly + +Use `IpAddresses.cidr` to define a Cidr range for your Vpc directly in code: + +```ts +import { IpAddresses } from '@aws-cdk/aws-ec2'; + +new ec2.Vpc(stack, 'TheVPC', { + ipAddresses: ec2.IpAddresses.cidr('10.0.1.0/20') +}); +``` + +Space will be allocated to subnets in the following order: + +* First, spaces is allocated for all subnets groups that explicitly have a `cidrMask` set as part of their configuration (including reserved subnets). +* Afterwards, any remaining space is divided evenly between the rest of the subnets (if any). + +The argument to `IpAddresses.cidr` may not be a token, and concrete Cidr values are generated in the synthesized CloudFormation template. + +#### Allocating an IP range from AWS IPAM + +Amazon VPC IP Address Manager (IPAM) manages a large IP space, from which chunks can be allocated for use in the Vpc. For information on Amazon VPC IP Address Manager please see the [official documentation](https://docs.aws.amazon.com/vpc/latest/ipam/what-it-is-ipam.html). An example of allocating from AWS IPAM looks like this: + +```ts +import { IpAddresses } from '@aws-cdk/aws-ec2'; + +declare const pool: ec2.CfnIPAMPool; + +new ec2.Vpc(stack, 'TheVPC', { + ipAddresses: ec2.IpAddresses.awsIpamAllocation({ + ipv4IpamPoolId: pool.ref, + ipv4NetmaskLength: 18, + defaultSubnetIpv4NetmaskLength: 24 + }) +}); +``` + +`IpAddresses.awsIpamAllocation` requires the following: + +* `ipv4IpamPoolId`, the id of an IPAM Pool from which the VPC range should be allocated. +* `ipv4NetmaskLength`, the size of the IP range that will be requested from the Pool at deploy time. +* `defaultSubnetIpv4NetmaskLength`, the size of subnets in groups that don't have `cidrMask` set. + +With this method of IP address management, no attempt is made to guess at subnet group sizes or to exhaustively allocate the IP range. All subnet groups must have an explicit `cidrMask` set as part of their subnet configuration, or `defaultSubnetIpv4NetmaskLength` must be set for a default size. If not, synthesis will fail and you must provide one or the other. + ### Advanced Subnet Configuration If the default VPC configuration (public and private subnets spanning the @@ -227,9 +283,9 @@ subnet configuration could look like this: ```ts const vpc = new ec2.Vpc(this, 'TheVPC', { - // 'cidr' configures the IP range and size of the entire VPC. - // The IP space will be divided over the configured subnets. - cidr: '10.0.0.0/21', + // 'IpAddresses' configures the IP range and size of the entire VPC. + // The IP space will be divided based on configuration for the subnets. + ipAddresses: IpAddresses.cidr('10.0.0.0/21'), // 'maxAzs' configures the maximum number of availability zones to use. // If you want to specify the exact availability zones you want the VPC @@ -948,11 +1004,11 @@ new ec2.Instance(this, 'Instance2', { }), }); -// AWS Linux 2 with kernel 5.x +// AWS Linux 2 with kernel 5.x new ec2.Instance(this, 'Instance3', { vpc, instanceType, - machineImage: new ec2.AmazonLinuxImage({ + machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, kernel: ec2.AmazonLinuxKernel.KERNEL5_X, }), @@ -962,7 +1018,7 @@ new ec2.Instance(this, 'Instance3', { new ec2.Instance(this, 'Instance4', { vpc, instanceType, - machineImage: new ec2.AmazonLinuxImage({ + machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2022, }), }); @@ -1407,9 +1463,9 @@ asset.grantRead(instance.role); ### Persisting user data By default, EC2 UserData is run once on only the first time that an instance is started. It is possible to make the -user data script run on every start of the instance. +user data script run on every start of the instance. -When creating a Windows UserData you can use the `persist` option to set whether or not to add +When creating a Windows UserData you can use the `persist` option to set whether or not to add `true` [to the user data script](https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2-windows-user-data.html#user-data-scripts). it can be used as follows: ```ts diff --git a/packages/@aws-cdk/aws-ec2/lib/cidr-splits.ts b/packages/@aws-cdk/aws-ec2/lib/cidr-splits.ts new file mode 100644 index 0000000000000..04d976007fc5c --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/lib/cidr-splits.ts @@ -0,0 +1,77 @@ +/** + * Return the splits necessary to allocate the given sequence of cidrs in the given order + * + * The entire block is of size 'rootNetmask', and subsequent blocks will be allocated + * from it sized according to the sizes in the 'netmasks' array. + * + * The return value is a list of `CidrSplit` objects, which represent + * invocations of a pair of `Fn.select(Fn.cidr(...))` operations. + * + * Strategy: walk through the IP block space, clipping to the next possible + * start of a block of the given size, then allocate it. Here is an unrealistic + * example (with a weird ordering of the netmasks to show how clipping and hence + * space wasting plays out in practice): + * + * root space /16 + * ┌──────────────────────────────────────────────────────────────────────────────────────────────┐ + * │ │ + * A /21 B /19 + * ┌───┬───┬───┬───┬───────────────┬───────────────┬───┬───────────┬───────────────┬──────────────┐ + * │ A │ A │ A │###│ B │ B │ A │###########│ B │ .... │ + * └───┴───┴───┴───┴───────────────┴───────────────┴───┴───────────┴───────────────┴──────────────┘ + * ^^^______ wasted space _________________^^^^^^ + */ +export function calculateCidrSplits(rootNetmask: number, netmasks: number[]): CidrSplit[] { + const ret = new Array(); + + let offset = 0; + for (const netmask of netmasks) { + const size = Math.pow(2, 32 - netmask); + + // Clip offset to the next block of the given size + offset = nextMultiple(offset, size); + + const count = Math.pow(2, netmask - rootNetmask); + ret.push({ + count, + netmask, + index: offset / size, + }); + + // Consume + offset += size; + } + + if (offset > Math.pow(2, 32 - rootNetmask)) { + throw new Error(`IP space of size /${rootNetmask} not big enough to allocate subnets of sizes ${netmasks.map(x => `/${x}`)}`); + } + + return ret; +} + +function nextMultiple(current: number, multiple: number) { + return Math.ceil(current / multiple) * multiple; +} + +/** + * A representation of a pair of `Fn.select(Fn.cidr())` invocations + */ +export interface CidrSplit { + /** + * The netmask of this block size + * + * This is the inverse number of what you need to pass to Fn.cidr (pass `32 - + * netmask` to Fn.cidr)`. + */ + readonly netmask: number; + + /** + * How many parts the mask needs to be split into + */ + readonly count: number; + + /** + * What subnet index to select from the split + */ + readonly index: number; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/lib/index.ts b/packages/@aws-cdk/aws-ec2/lib/index.ts index 4b0741044e4dd..ce50e5dff927e 100644 --- a/packages/@aws-cdk/aws-ec2/lib/index.ts +++ b/packages/@aws-cdk/aws-ec2/lib/index.ts @@ -27,6 +27,7 @@ export * from './client-vpn-endpoint-types'; export * from './client-vpn-endpoint'; export * from './client-vpn-authorization-rule'; export * from './client-vpn-route'; +export * from './ip-addresses'; // AWS::EC2 CloudFormation Resources: export * from './ec2.generated'; diff --git a/packages/@aws-cdk/aws-ec2/lib/ip-addresses.ts b/packages/@aws-cdk/aws-ec2/lib/ip-addresses.ts new file mode 100644 index 0000000000000..4a6cbc6df7ea7 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/lib/ip-addresses.ts @@ -0,0 +1,312 @@ +import { Fn, Token } from '@aws-cdk/core'; +import { calculateCidrSplits } from './cidr-splits'; +import { NetworkBuilder } from './network-util'; +import { SubnetConfiguration } from './vpc'; + +/** + * An abstract Provider of IpAddresses + */ +export class IpAddresses { + /** + * Used to provide local Ip Address Management services for your VPC + * + * VPC Cidr is supplied at creation and subnets are calculated locally + * + */ + public static cidr(cidrBlock: string): IIpAddresses { + return new Cidr(cidrBlock); + } + + /** + * Used to provide centralized Ip Address Management services for your VPC + * + * Uses VPC Cidr allocations from AWS IPAM + * + * @see https://docs.aws.amazon.com/vpc/latest/ipam/what-it-is-ipam.html + */ + public static awsIpamAllocation(props: AwsIpamProps): IIpAddresses { + return new AwsIpam(props); + } + + private constructor() { } +} + +/** + * Implementations for ip address management + */ +export interface IIpAddresses { + /** + * Called by the VPC to retrieve VPC options from the Ipam + * + * Don't call this directly, the VPC will call it automatically. + */ + allocateVpcCidr(): VpcIpamOptions; + + /** + * Called by the VPC to retrieve Subnet options from the Ipam + * + * Don't call this directly, the VPC will call it automatically. + */ + allocateSubnetsCidr(input: AllocateCidrRequest): SubnetIpamOptions; +} + +/** + * Cidr Allocated Vpc + */ +export interface VpcIpamOptions { + + /** + * Cidr Block for Vpc + * + * @default - Only required when Ipam has concrete allocation available for static Vpc + */ + readonly cidrBlock?: string; + + /** + * Cidr Mask for Vpc + * + * @default - Only required when using AWS Ipam + */ + readonly ipv4NetmaskLength?: number; + + /** + * ipv4 IPAM Pool Id + * + * @default - Only required when using AWS Ipam + */ + readonly ipv4IpamPoolId?: string; +} + +/** + * Subnet requested for allocation + */ +export interface RequestedSubnet { + + /** + * The availability zone for the subnet + */ + readonly availabilityZone: string; + + /** + * Specify configuration parameters for a single subnet group in a VPC + */ + readonly configuration: SubnetConfiguration; + + /** + * Id for the Subnet construct + */ + readonly subnetConstructId: string; +} + +/** + * An instance of a Subnet requested for allocation + */ +interface IRequestedSubnetInstance { + /** + * Index location of Subnet requested for allocation + */ + readonly index: number, + + /** + * Subnet requested for allocation + */ + readonly requestedSubnet: RequestedSubnet +} + +/** + * Request for subnets Cidr to be allocated for a Vpc + */ +export interface AllocateCidrRequest { + + /** + * The IPv4 CIDR block for this Vpc + */ + readonly vpcCidr: string; + + /** + * The Subnets to be allocated + */ + readonly requestedSubnets: RequestedSubnet[]; +} + +/** + * Cidr Allocated Subnets + */ +export interface SubnetIpamOptions { + /** + * Cidr Allocations for Subnets + */ + readonly allocatedSubnets: AllocatedSubnet[]; +} + +/** + * Cidr Allocated Subnet + */ +export interface AllocatedSubnet { + /** + * Cidr Allocations for a Subnet + */ + readonly cidr: string; +} + +/** + * Configuration for AwsIpam + */ +export interface AwsIpamProps { + + /** + * Netmask length for Vpc + */ + readonly ipv4NetmaskLength: number; + + /** + * Ipam Pool Id for ipv4 allocation + */ + readonly ipv4IpamPoolId: string; // todo: should be a type + + /** + * Default length for Subnet ipv4 Network mask + * + * Specify this option only if you do not specify all Subnets using SubnetConfiguration with a cidrMask + * + * @default - Default ipv4 Subnet Mask for subnets in Vpc + * + */ + readonly defaultSubnetIpv4NetmaskLength?: number; +} + +/** + * Implements integration with Amazon VPC IP Address Manager (IPAM). + * + * See the package-level documentation of this package for an overview + * of the various dimensions in which you can configure your VPC. + * + * For example: + * + * ```ts + * new ec2.Vpc(stack, 'TheVPC', { + * ipAddresses: IpAddresses.awsIpamAllocation({ + * ipv4IpamPoolId: pool.ref, + * ipv4NetmaskLength: 18, + * defaultSubnetIpv4NetmaskLength: 24 + * }) + * }); + * ``` + * + */ +class AwsIpam implements IIpAddresses { + constructor(private readonly props: AwsIpamProps) { + } + + /** + * Allocates Vpc Cidr. called when creating a Vpc using AwsIpam. + */ + allocateVpcCidr(): VpcIpamOptions { + return { + ipv4NetmaskLength: this.props.ipv4NetmaskLength, + ipv4IpamPoolId: this.props.ipv4IpamPoolId, + }; + } + + /** + * Allocates Subnets Cidrs. Called by VPC when creating subnets. + */ + allocateSubnetsCidr(input: AllocateCidrRequest): SubnetIpamOptions { + + const cidrSplit = calculateCidrSplits(this.props.ipv4NetmaskLength, input.requestedSubnets.map((mask => { + + if ((mask.configuration.cidrMask === undefined) && (this.props.defaultSubnetIpv4NetmaskLength=== undefined) ) { + throw new Error('If you have not set a cidr for all subnets in this case you must set a defaultCidrMask in AwsIpam Options'); + } + + const cidrMask = mask.configuration.cidrMask ?? this.props.defaultSubnetIpv4NetmaskLength; + + if (cidrMask === undefined) { + throw new Error('Should not have happened, but satisfies the type checker'); + } + + return cidrMask; + }))); + + const allocatedSubnets: AllocatedSubnet[] = cidrSplit.map(subnet => { + return { + cidr: Fn.select(subnet.index, Fn.cidr(input.vpcCidr, subnet.count, `${32-subnet.netmask}`)), + }; + }); + + return { + allocatedSubnets: allocatedSubnets, + }; + + } +} + +/** + * Implements static Ip assignment locally. + * + * See the package-level documentation of this package for an overview + * of the various dimensions in which you can configure your VPC. + * + * For example: + * + * ```ts + * new ec2.Vpc(stack, 'TheVPC', { + * ipAddresses: ec2.IpAddresses.cidr('10.0.1.0/20') + * }); + * ``` + * + */ +class Cidr implements IIpAddresses { + private readonly networkBuilder: NetworkBuilder; + + constructor(private readonly cidrBlock: string) { + if (Token.isUnresolved(cidrBlock)) { + throw new Error('\'cidr\' property must be a concrete CIDR string, got a Token (we need to parse it for automatic subdivision)'); + } + + this.networkBuilder = new NetworkBuilder(this.cidrBlock); + } + + /** + * Allocates Vpc Cidr. called when creating a Vpc using IpAddresses.cidr. + */ + allocateVpcCidr(): VpcIpamOptions { + return { + cidrBlock: this.networkBuilder.networkCidr.cidr, + }; + } + + /** + * Allocates Subnets Cidrs. Called by VPC when creating subnets. + */ + allocateSubnetsCidr(input: AllocateCidrRequest): SubnetIpamOptions { + + const allocatedSubnets: AllocatedSubnet[] = []; + const subnetsWithoutDefinedCidr: IRequestedSubnetInstance[] = []; + //default: Available IP space is evenly divided across subnets if no cidr is given. + + input.requestedSubnets.forEach((requestedSubnet, index) => { + if (requestedSubnet.configuration.cidrMask === undefined) { + subnetsWithoutDefinedCidr.push({ + index, + requestedSubnet, + }); + } else { + allocatedSubnets.push({ + cidr: this.networkBuilder.addSubnet(requestedSubnet.configuration.cidrMask), + }); + } + }); + + const cidrMaskForRemaining = this.networkBuilder.maskForRemainingSubnets(subnetsWithoutDefinedCidr.length); + subnetsWithoutDefinedCidr.forEach((subnet)=> { + allocatedSubnets.splice(subnet.index, 0, { + cidr: this.networkBuilder.addSubnet(cidrMaskForRemaining), + }); + }); + + return { + allocatedSubnets: allocatedSubnets, + }; + } +} diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 61dba97493a58..719c1f9df4bdf 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -10,9 +10,9 @@ import { CfnEIP, CfnInternetGateway, CfnNatGateway, CfnRoute, CfnRouteTable, CfnSubnet, CfnSubnetRouteTableAssociation, CfnVPC, CfnVPCGatewayAttachment, CfnVPNGatewayRoutePropagation, } from './ec2.generated'; +import { AllocatedSubnet, IIpAddresses, RequestedSubnet, IpAddresses } from './ip-addresses'; import { NatProvider } from './nat'; import { INetworkAcl, NetworkAcl, SubnetNetworkAclAssociation } from './network-acl'; -import { NetworkBuilder } from './network-util'; import { SubnetFilter } from './subnet'; import { allRouteTableIds, defaultSubnetName, flatten, ImportSubnetGroup, subnetGroupNameFromConstructId, subnetId } from './util'; import { GatewayVpcEndpoint, GatewayVpcEndpointAwsService, GatewayVpcEndpointOptions, InterfaceVpcEndpoint, InterfaceVpcEndpointOptions } from './vpc-endpoint'; @@ -811,6 +811,15 @@ const NAME_TAG: string = 'Name'; */ export interface VpcProps { + /** + * The Provider to use to allocate IP Space to your VPC. + * + * Options include static allocation or from a pool. + * + * @default ec2.IpAddresses.cidr + */ + readonly ipAddresses?: IIpAddresses; + /** * The CIDR range to use for the VPC, e.g. '10.0.0.0/16'. * @@ -818,6 +827,8 @@ export interface VpcProps { * split across all subnets per Availability Zone. * * @default Vpc.DEFAULT_CIDR_RANGE + * + * @deprecated Use ipAddresses instead */ readonly cidr?: string; @@ -1090,7 +1101,7 @@ export interface SubnetConfiguration { * * ```ts * const vpc = new ec2.Vpc(this, 'TheVPC', { - * cidr: "10.0.0.0/16" + * ipAddresses: IpAddresses.cidr('10.0.0.0/16'), * }) * * // Iterate the private subnets @@ -1303,9 +1314,9 @@ export class Vpc extends VpcBase { private readonly resource: CfnVPC; /** - * The NetworkBuilder + * The provider of ip addresses */ - private networkBuilder: NetworkBuilder; + private readonly ipAddresses: IIpAddresses; /** * Subnet configurations for this VPC @@ -1339,16 +1350,24 @@ export class Vpc extends VpcBase { throw new Error('\'cidr\' property must be a concrete CIDR string, got a Token (we need to parse it for automatic subdivision)'); } - this.networkBuilder = new NetworkBuilder(cidrBlock); + if (props.ipAddresses && props.cidr) { + throw new Error('supply at most one of ipAddresses or cidr'); + } + + this.ipAddresses = props.ipAddresses ?? IpAddresses.cidr(cidrBlock); this.dnsHostnamesEnabled = props.enableDnsHostnames == null ? true : props.enableDnsHostnames; this.dnsSupportEnabled = props.enableDnsSupport == null ? true : props.enableDnsSupport; const instanceTenancy = props.defaultInstanceTenancy || 'default'; this.internetConnectivityEstablished = this._internetConnectivityEstablished; + const vpcIpAddressOptions = this.ipAddresses.allocateVpcCidr(); + // Define a VPC using the provided CIDR range this.resource = new CfnVPC(this, 'Resource', { - cidrBlock, + cidrBlock: vpcIpAddressOptions.cidrBlock, + ipv4IpamPoolId: vpcIpAddressOptions.ipv4IpamPoolId, + ipv4NetmaskLength: vpcIpAddressOptions.ipv4NetmaskLength, enableDnsHostnames: this.dnsHostnamesEnabled, enableDnsSupport: this.dnsSupportEnabled, instanceTenancy, @@ -1508,28 +1527,38 @@ export class Vpc extends VpcBase { * array or creates the `DEFAULT_SUBNETS` configuration */ private createSubnets() { - const remainingSpaceSubnets: SubnetConfiguration[] = []; - for (const subnet of this.subnetConfiguration) { - if (subnet.cidrMask === undefined) { - remainingSpaceSubnets.push(subnet); - continue; - } - this.createSubnetResources(subnet, subnet.cidrMask); - } + const requestedSubnets: RequestedSubnet[] = []; - const totalRemaining = remainingSpaceSubnets.length * this.availabilityZones.length; - const cidrMaskForRemaining = this.networkBuilder.maskForRemainingSubnets(totalRemaining); - for (const subnet of remainingSpaceSubnets) { - this.createSubnetResources(subnet, cidrMaskForRemaining); + this.subnetConfiguration.forEach((configuration)=> ( + this.availabilityZones.forEach((az, index) => { + requestedSubnets.push({ + availabilityZone: az, + subnetConstructId: subnetId(configuration.name, index), + configuration, + }); + }, + ))); + + const { allocatedSubnets } = this.ipAddresses.allocateSubnetsCidr({ + vpcCidr: this.vpcCidrBlock, + requestedSubnets, + }); + + if (allocatedSubnets.length != requestedSubnets.length) { + throw new Error('Incomplete Subnet Allocation; response array dose not equal input array'); } + + this.createSubnetResources(requestedSubnets, allocatedSubnets); } - private createSubnetResources(subnetConfig: SubnetConfiguration, cidrMask: number) { - this.availabilityZones.forEach((zone, index) => { + private createSubnetResources(requestedSubnets: RequestedSubnet[], allocatedSubnets: AllocatedSubnet[]) { + allocatedSubnets.forEach((allocated, i) => { + + const { configuration: subnetConfig, subnetConstructId, availabilityZone } = requestedSubnets[i]; + if (subnetConfig.reserved === true) { - // For reserved subnets, just allocate ip space but do not create any resources - this.networkBuilder.addSubnet(cidrMask); + // For reserved subnets, do not create any resources return; } @@ -1544,31 +1573,30 @@ export class Vpc extends VpcBase { : true; } - const name = subnetId(subnetConfig.name, index); const subnetProps: SubnetProps = { - availabilityZone: zone, + availabilityZone, vpcId: this.vpcId, - cidrBlock: this.networkBuilder.addSubnet(cidrMask), + cidrBlock: allocated.cidr, mapPublicIpOnLaunch: mapPublicIpOnLaunch, }; let subnet: Subnet; switch (subnetConfig.subnetType) { case SubnetType.PUBLIC: - const publicSubnet = new PublicSubnet(this, name, subnetProps); + const publicSubnet = new PublicSubnet(this, subnetConstructId, subnetProps); this.publicSubnets.push(publicSubnet); subnet = publicSubnet; break; case SubnetType.PRIVATE_WITH_EGRESS: case SubnetType.PRIVATE_WITH_NAT: case SubnetType.PRIVATE: - const privateSubnet = new PrivateSubnet(this, name, subnetProps); + const privateSubnet = new PrivateSubnet(this, subnetConstructId, subnetProps); this.privateSubnets.push(privateSubnet); subnet = privateSubnet; break; case SubnetType.PRIVATE_ISOLATED: case SubnetType.ISOLATED: - const isolatedSubnet = new PrivateSubnet(this, name, subnetProps); + const isolatedSubnet = new PrivateSubnet(this, subnetConstructId, subnetProps); this.isolatedSubnets.push(isolatedSubnet); subnet = isolatedSubnet; break; diff --git a/packages/@aws-cdk/aws-ec2/test/cidr-splits.test.ts b/packages/@aws-cdk/aws-ec2/test/cidr-splits.test.ts new file mode 100644 index 0000000000000..af7d43dde687e --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/cidr-splits.test.ts @@ -0,0 +1,39 @@ +import { calculateCidrSplits } from '../lib/cidr-splits'; + +describe('Cidr split results', () => { + test('3 big subnets then 6 small ones', () => { + expect(calculateCidrSplits(22, [24, 24, 24, 28, 28, 28, 28, 28, 28])).toEqual([ + { netmask: 24, count: 4, index: 0 }, + { netmask: 24, count: 4, index: 1 }, + { netmask: 24, count: 4, index: 2 }, + { netmask: 28, count: 64, index: 48 }, + { netmask: 28, count: 64, index: 49 }, + { netmask: 28, count: 64, index: 50 }, + { netmask: 28, count: 64, index: 51 }, + { netmask: 28, count: 64, index: 52 }, + { netmask: 28, count: 64, index: 53 }, + ]); + }); + + test('3 small subnets then 2 big ones', () => { + expect(calculateCidrSplits(22, [27, 27, 27, 24, 24])).toEqual([ + { netmask: 27, count: 32, index: 0 }, + { netmask: 27, count: 32, index: 1 }, + { netmask: 27, count: 32, index: 2 }, + { netmask: 24, count: 4, index: 1 }, + { netmask: 24, count: 4, index: 2 }, + ]); + }); + + test('small big small', () => { + expect (calculateCidrSplits(22, [28, 24, 28])).toEqual([ + { netmask: 28, count: 64, index: 0 }, + { netmask: 24, count: 4, index: 1 }, + { netmask: 28, count: 64, index: 32 }, + ]); + }); + + test('allocation too big', () => { + expect(() => calculateCidrSplits(22, [23, 23, 23])).toThrow(/not big enough/); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.reserved-private-subnet.ts b/packages/@aws-cdk/aws-ec2/test/integ.reserved-private-subnet.ts index a19e9864b257e..583983e0aef2a 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.reserved-private-subnet.ts +++ b/packages/@aws-cdk/aws-ec2/test/integ.reserved-private-subnet.ts @@ -19,7 +19,7 @@ class VpcReservedPrivateSubnetStack extends cdk.Stack { /// !show // Specify no NAT gateways with a reserved private subnet new ec2.Vpc(this, 'VPC', { - cidr: '10.0.0.0/16', + ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'), subnetConfiguration: [ { name: 'ingress', diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc-ipam.ts b/packages/@aws-cdk/aws-ec2/test/integ.vpc-ipam.ts new file mode 100644 index 0000000000000..7b4f3296c09f4 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc-ipam.ts @@ -0,0 +1,83 @@ +import * as cdk from '@aws-cdk/core'; +import { ExpectedResult, IntegTest } from '@aws-cdk/integ-tests'; +import { IpAddresses, CfnIPAM, CfnIPAMPool, CfnVPC, SubnetType, Vpc } from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-ec2-ipam-vpc'); + +/** + * ### MANUAL CLEAN UP REQUIRED ### + * + * When IPAM is created running this integ-test it is not currently removed after the test run is complete. + * + */ + +const ipam = new CfnIPAM(stack, 'IPAM', { + operatingRegions: [ + { regionName: stack.region }, + ], + tags: [{ + key: 'stack', + value: stack.stackId, + }], +}); +ipam.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN); + +const pool = new CfnIPAMPool(stack, 'Pool', { + description: 'Testing pool', + addressFamily: 'ipv4', + autoImport: false, + locale: stack.region, + ipamScopeId: ipam.attrPrivateDefaultScopeId, + provisionedCidrs: [{ + cidr: '100.100.0.0/16', + }], +}); +pool.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN); + +const awsIpamVpc = new Vpc(stack, 'AwsIpamVpc', { + ipAddresses: IpAddresses.awsIpamAllocation({ + ipv4IpamPoolId: pool.ref, + ipv4NetmaskLength: 18, + defaultSubnetIpv4NetmaskLength: 24, + }), + maxAzs: 2, + subnetConfiguration: [{ + name: 'private', + subnetType: SubnetType.PRIVATE_ISOLATED, + cidrMask: 24, + }], +}); + +// needs AwsApiCall Support for installLatestAwsSdk first, or another way to clean the Ipam +// new AwsApiCall(stack, 'cleanUpIpam', { +// service: 'EC2', +// api: 'deleteIpam', +// installLatestAwsSdk: true, +// parameters: { +// IpamId: ipam.attrIpamId, +// Cascade: true, +// }, +// }); + +/** + * Testing That the Vpc is Deployed with the correct Cidrs. +**/ +const integ = new IntegTest(app, 'Vpc-Ipam', { + testCases: [stack], + allowDestroy: ['EC2::IPAM'], +}); + +integ.assertions.awsApiCall('EC2', 'describeVpcs', { + VpcIds: [(awsIpamVpc.node.defaultChild as CfnVPC).getAtt('VpcId').toString()], +}).expect(ExpectedResult.objectLike({ + Vpcs: [ + { + CidrBlock: '100.100.0.0/18', + }, + ], +})); + +app.synth(); + + diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpn-pre-shared-key-token.ts b/packages/@aws-cdk/aws-ec2/test/integ.vpn-pre-shared-key-token.ts index c3d06a2e302f1..24d97423bf033 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpn-pre-shared-key-token.ts +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpn-pre-shared-key-token.ts @@ -5,7 +5,7 @@ const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-cdk-ec2-vpn'); const vpc = new ec2.Vpc(stack, 'MyVpc', { - cidr: '10.10.0.0/16', + ipAddresses: ec2.IpAddresses.cidr('10.10.0.0/16'), vpnConnections: { Dynamic: { // Dynamic routing ip: '52.85.255.164', diff --git a/packages/@aws-cdk/aws-ec2/test/ip-addresses.test.ts b/packages/@aws-cdk/aws-ec2/test/ip-addresses.test.ts new file mode 100644 index 0000000000000..4df3561fd4b82 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/ip-addresses.test.ts @@ -0,0 +1,411 @@ + +import { Template } from '@aws-cdk/assertions'; +import { Stack } from '@aws-cdk/core'; +import { IpAddresses, SubnetType, Vpc } from '../lib'; + +describe('Cidr vpc allocation', () => { + + test('Default Cidr returns the correct vpc cidr', () => { + const ipAddresses = IpAddresses.cidr('10.0.0.0/16'); + expect(ipAddresses.allocateVpcCidr().cidrBlock).toEqual('10.0.0.0/16'); + }); + + test('Default Cidr returns ipv4IpamPoolId as undefined', () => { + const ipAddresses = IpAddresses.cidr('10.0.0.0/16'); + expect(ipAddresses.allocateVpcCidr().ipv4IpamPoolId).toBeUndefined; + }); + + test('Default Cidr returns ipv4NetmaskLength as undefined', () => { + const ipAddresses = IpAddresses.cidr('10.0.0.0/16'); + expect(ipAddresses.allocateVpcCidr().ipv4NetmaskLength).toBeUndefined; + }); + +}); + +describe('IpAddresses.cidr subnets allocation', () => { + + const cidrProps = '10.0.0.0/16'; + + test('Default Cidr returns the correct subnet allocations, when you do not give a cidr for the subnets', () => { + const ipAddresses = IpAddresses.cidr(cidrProps); + expect(ipAddresses.allocateSubnetsCidr({ + requestedSubnets: [{ + availabilityZone: 'dummyAz1', + configuration: { + name: 'public', + subnetType: SubnetType.PUBLIC, + }, + subnetConstructId: 'public', + }, { + availabilityZone: 'dummyAz1', + configuration: { + name: 'private-with-egress', + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + }, + subnetConstructId: 'public', + }], + vpcCidr: '10.0.0.0/16', + }).allocatedSubnets).toEqual([{ cidr: '10.0.0.0/17' }, { cidr: '10.0.128.0/17' }]); + }); + + test('Default Cidr returns the correct subnet allocations, when you provide a cidr for the subnets', () => { + const ipAddresses = IpAddresses.cidr(cidrProps); + expect(ipAddresses.allocateSubnetsCidr({ + requestedSubnets: [{ + availabilityZone: 'dummyAz1', + configuration: { + name: 'public', + subnetType: SubnetType.PUBLIC, + cidrMask: 24, + }, + subnetConstructId: 'public', + }, { + availabilityZone: 'dummyAz1', + configuration: { + name: 'private-with-egress', + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + cidrMask: 24, + }, + subnetConstructId: 'public', + }], + vpcCidr: '10.0.0.0/16', + }).allocatedSubnets).toEqual([{ cidr: '10.0.0.0/24' }, { cidr: '10.0.1.0/24' }]); + }); + + test('Default Cidr returns the correct subnet allocations, when you mix provided and non provided cidr for the subnets', () => { + const ipAddresses = IpAddresses.cidr(cidrProps); + expect(ipAddresses.allocateSubnetsCidr({ + requestedSubnets: [{ + availabilityZone: 'dummyAz1', + configuration: { + name: 'public', + subnetType: SubnetType.PUBLIC, + }, + subnetConstructId: 'public', + }, { + availabilityZone: 'dummyAz1', + configuration: { + name: 'private-with-egress', + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + cidrMask: 24, + }, + subnetConstructId: 'public', + }], + vpcCidr: '10.0.0.0/16', + }).allocatedSubnets).toEqual([{ cidr: '10.0.128.0/17' }, { cidr: '10.0.0.0/24' }]); + }); + +}); + +describe('AwsIpam vpc allocation', () => { + + const awsIpamProps = { + ipv4IpamPoolId: 'ipam-pool-0111222333444', + ipv4NetmaskLength: 22, + }; + + test('AwsIpam returns cidrBlock as undefined', () => { + const ipAddresses = IpAddresses.awsIpamAllocation(awsIpamProps); + expect(ipAddresses.allocateVpcCidr().cidrBlock).toBeUndefined; + }); + + test('AwsIpam returns the correct vpc ipv4IpamPoolId', () => { + const ipAddresses = IpAddresses.awsIpamAllocation(awsIpamProps); + expect(ipAddresses.allocateVpcCidr().ipv4IpamPoolId).toEqual('ipam-pool-0111222333444'); + }); + + test('AwsIpam returns the correct vpc ipv4NetmaskLength', () => { + const ipAddresses = IpAddresses.awsIpamAllocation(awsIpamProps); + expect(ipAddresses.allocateVpcCidr().ipv4NetmaskLength).toEqual(22); + }); + +}); + +describe('AwsIpam subnets allocation', () => { + + const awsIpamProps = { + ipv4IpamPoolId: 'ipam-pool-0111222333444', + ipv4NetmaskLength: 22, + }; + + test('AwsIpam returns subnet allocations as 2x TOKEN, when you do not give a cidr for the subnets', () => { + const ipAddresses = IpAddresses.awsIpamAllocation({ defaultSubnetIpv4NetmaskLength: 24, ...awsIpamProps }); + const allocations = ipAddresses.allocateSubnetsCidr({ + requestedSubnets: [{ + availabilityZone: 'dummyAz1', + configuration: { + name: 'public', + subnetType: SubnetType.PUBLIC, + }, + subnetConstructId: 'public', + }, { + availabilityZone: 'dummyAz1', + configuration: { + name: 'private-with-egress', + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + }, + subnetConstructId: 'public', + }], + vpcCidr: '10.0.0.0/16', + }); + + expect (allocations.allocatedSubnets.length).toBe(2); + expect (allocations.allocatedSubnets[0].cidr).toContain('TOKEN'); + expect (allocations.allocatedSubnets[1].cidr).toContain('TOKEN'); + }); + + test('AwsIpam returns subnet allocations as 2x TOKEN, when you provide a cidr for the subnets', () => { + const ipAddresses = IpAddresses.awsIpamAllocation(awsIpamProps); + const allocations = ipAddresses.allocateSubnetsCidr({ + requestedSubnets: [{ + availabilityZone: 'dummyAz1', + configuration: { + name: 'public', + subnetType: SubnetType.PUBLIC, + cidrMask: 24, + }, + subnetConstructId: 'public', + }, { + availabilityZone: 'dummyAz1', + configuration: { + name: 'private-with-egress', + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + cidrMask: 24, + }, + subnetConstructId: 'public', + }], + vpcCidr: '10.0.0.0/16', + }); + + expect (allocations.allocatedSubnets.length).toBe(2); + expect (allocations.allocatedSubnets[0].cidr).toContain('TOKEN'); + expect (allocations.allocatedSubnets[1].cidr).toContain('TOKEN'); + }); + + test('AwsIpam returns subnet allocations as 2x TOKEN, when you mix provide and non provided cidr for the subnets', () => { + const ipAddresses = IpAddresses.awsIpamAllocation({ defaultSubnetIpv4NetmaskLength: 24, ...awsIpamProps }); + const allocations = ipAddresses.allocateSubnetsCidr({ + requestedSubnets: [{ + availabilityZone: 'dummyAz1', + configuration: { + name: 'public', + subnetType: SubnetType.PUBLIC, + }, + subnetConstructId: 'public', + }, { + availabilityZone: 'dummyAz1', + configuration: { + name: 'private-with-egress', + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + cidrMask: 24, + }, + subnetConstructId: 'public', + }], + vpcCidr: '10.0.0.0/16', + }); + + expect (allocations.allocatedSubnets.length).toBe(2); + expect (allocations.allocatedSubnets[0].cidr).toContain('TOKEN'); + expect (allocations.allocatedSubnets[1].cidr).toContain('TOKEN'); + }); + +}); + +describe('IpAddresses.cidr Vpc Integration', () => { + test('IpAddresses.cidr provides the correct Cidr allocation to the Vpc ', () => { + + const stack = new Stack(); + + const cidrProps = '10.0.0.0/16'; + const ipAddresses = IpAddresses.cidr(cidrProps); + + new Vpc(stack, 'VpcNetwork', { ipAddresses: ipAddresses }); + + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', { + CidrBlock: cidrProps, + }); + }); + + test('IpAddresses.cidr provides the correct Subnet allocation to the Vpc', () => { + + const stack = new Stack(); + + const cidrProps = '10.0.0.0/16'; + const ipAddresses = IpAddresses.cidr(cidrProps); + + new Vpc(stack, 'VpcNetwork', { ipAddresses: ipAddresses }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::EC2::Subnet', { + CidrBlock: '10.0.0.0/18', + }); + template.hasResourceProperties('AWS::EC2::Subnet', { + CidrBlock: '10.0.64.0/18', + }); + template.hasResourceProperties('AWS::EC2::Subnet', { + CidrBlock: '10.0.128.0/18', + }); + template.hasResourceProperties('AWS::EC2::Subnet', { + CidrBlock: '10.0.192.0/18', + }); + }); +}); + +describe('AwsIpam Vpc Integration', () => { + + test('Should throw if there are subnets without explicit Cidr and no defaultCidr given', () => { + + const stack = new Stack(); + + const awsIpamProps = { + ipv4IpamPoolId: 'ipam-pool-0111222333444', + ipv4NetmaskLength: 22, + }; + + const ipAddresses = IpAddresses.awsIpamAllocation(awsIpamProps); + + expect(() => {new Vpc(stack, 'VpcNetwork', { ipAddresses: ipAddresses });}).toThrow(/If you have not set a cidr for all subnets in this case you must set a defaultCidrMask in AwsIpam Options/);; + + }); + + test('AwsIpam provides the correct Cidr allocation to the Vpc ', () => { + + const stack = new Stack(); + + const awsIpamProps = { + ipv4IpamPoolId: 'ipam-pool-0111222333444', + ipv4NetmaskLength: 22, + defaultSubnetIpv4NetmaskLength: 24, + }; + + const ipAddresses = IpAddresses.awsIpamAllocation(awsIpamProps); + + new Vpc(stack, 'VpcNetwork', { ipAddresses: ipAddresses }); + + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', { + Ipv4IpamPoolId: awsIpamProps.ipv4IpamPoolId, + Ipv4NetmaskLength: awsIpamProps.ipv4NetmaskLength, + }); + }); + + + test('AwsIpam provides the correct Subnet allocation to the Vpc', () => { + + const stack = new Stack(); + + const awsIpamProps = { + ipv4IpamPoolId: 'ipam-pool-0111222333444', + ipv4NetmaskLength: 22, + defaultSubnetIpv4NetmaskLength: 24, + }; + + const ipAddresses = IpAddresses.awsIpamAllocation(awsIpamProps); + + new Vpc(stack, 'VpcNetwork', { ipAddresses: ipAddresses }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::EC2::Subnet', { + CidrBlock: { + 'Fn::Select': [0, { + 'Fn::Cidr': [{ + 'Fn::GetAtt': ['VpcNetworkB258E83A', 'CidrBlock'], + }, 4, '8'], + }], + }, + }); + template.hasResourceProperties('AWS::EC2::Subnet', { + CidrBlock: { + 'Fn::Select': [1, { + 'Fn::Cidr': [{ + 'Fn::GetAtt': ['VpcNetworkB258E83A', 'CidrBlock'], + }, 4, '8'], + }], + }, + }); + template.hasResourceProperties('AWS::EC2::Subnet', { + CidrBlock: { + 'Fn::Select': [2, { + 'Fn::Cidr': [{ + 'Fn::GetAtt': ['VpcNetworkB258E83A', 'CidrBlock'], + }, 4, '8'], + }], + }, + }); + template.hasResourceProperties('AWS::EC2::Subnet', { + CidrBlock: { + 'Fn::Select': [3, { + 'Fn::Cidr': [{ + 'Fn::GetAtt': ['VpcNetworkB258E83A', 'CidrBlock'], + }, 4, '8'], + }], + }, + }); + }); + + test('Should throw if ipv4NetmaskLength not big enough to allocate subnets', () => { + + const stack = new Stack(); + + const awsIpamProps = { + ipv4IpamPoolId: 'ipam-pool-0111222333444', + ipv4NetmaskLength: 18, + defaultSubnetIpv4NetmaskLength: 17, + }; + + const ipAddresses = IpAddresses.awsIpamAllocation(awsIpamProps); + + expect(() => {new Vpc(stack, 'VpcNetwork', { ipAddresses: ipAddresses });}).toThrow('IP space of size /18 not big enough to allocate subnets of sizes /17,/17,/17,/17');; + + }); + + test('Should be able to allocate subnets from a SubnetConfiguration in Vpc Constructor', () => { + + const stack = new Stack(); + + const awsIpamProps = { + ipv4IpamPoolId: 'ipam-pool-0111222333444', + ipv4NetmaskLength: 18, + defaultSubnetIpv4NetmaskLength: 17, + }; + + const ipAddresses = IpAddresses.awsIpamAllocation(awsIpamProps); + + new Vpc(stack, 'VpcNetwork', { + ipAddresses: ipAddresses, + subnetConfiguration: [{ + name: 'public', + subnetType: SubnetType.PUBLIC, + cidrMask: 24, + }], + maxAzs: 2, + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::EC2::Subnet', { + CidrBlock: { + 'Fn::Select': [0, { + 'Fn::Cidr': [{ + 'Fn::GetAtt': ['VpcNetworkB258E83A', 'CidrBlock'], + }, 64, '8'], + }], + }, + }); + + template.hasResourceProperties('AWS::EC2::Subnet', { + CidrBlock: { + 'Fn::Select': [1, { + 'Fn::Cidr': [{ + 'Fn::GetAtt': ['VpcNetworkB258E83A', 'CidrBlock'], + }, 64, '8'], + }], + }, + }); + + template.resourceCountIs('AWS::EC2::Subnet', 2); + + }); + +}); diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/VpcIpamDefaultTestDeployAssertB1CA1C3A.assets.json b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/VpcIpamDefaultTestDeployAssertB1CA1C3A.assets.json new file mode 100644 index 0000000000000..2c3f67423ff8b --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/VpcIpamDefaultTestDeployAssertB1CA1C3A.assets.json @@ -0,0 +1,32 @@ +{ + "version": "21.0.0", + "files": { + "d47f7e6772bfdf47ecbc070ffe204baf53bacbfbf7814eb407bd8ea108c1c1bb": { + "source": { + "path": "asset.d47f7e6772bfdf47ecbc070ffe204baf53bacbfbf7814eb407bd8ea108c1c1bb.bundle", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "d47f7e6772bfdf47ecbc070ffe204baf53bacbfbf7814eb407bd8ea108c1c1bb.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "5ee44ec2182f45d2de6a0f0dce2ba54c61792d88196caa30d00380d2a5bb40c2": { + "source": { + "path": "VpcIpamDefaultTestDeployAssertB1CA1C3A.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "5ee44ec2182f45d2de6a0f0dce2ba54c61792d88196caa30d00380d2a5bb40c2.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/VpcIpamDefaultTestDeployAssertB1CA1C3A.template.json b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/VpcIpamDefaultTestDeployAssertB1CA1C3A.template.json new file mode 100644 index 0000000000000..36052e44320fd --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/VpcIpamDefaultTestDeployAssertB1CA1C3A.template.json @@ -0,0 +1,134 @@ +{ + "Resources": { + "AwsApiCallEC2describeVpcs": { + "Type": "Custom::DeployAssert@SdkCallEC2describeVpcs", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "service": "EC2", + "api": "describeVpcs", + "expected": "{\"$ObjectLike\":{\"Vpcs\":[{\"CidrBlock\":\"100.100.0.0/18\"}]}}", + "parameters": { + "VpcIds": [ + { + "Fn::ImportValue": "aws-cdk-ec2-ipam-vpc:ExportsOutputFnGetAttAwsIpamVpcD3A1DAEEVpcId808CC597" + } + ] + }, + "flattenResponse": "false", + "salt": "1666602022682" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "ec2:DescribeVpcs" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + } + ] + } + } + ] + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "d47f7e6772bfdf47ecbc070ffe204baf53bacbfbf7814eb407bd8ea108c1c1bb.zip" + }, + "Timeout": 120, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73", + "Arn" + ] + } + } + } + }, + "Outputs": { + "AssertionResultsAwsApiCallEC2describeVpcs": { + "Value": { + "Fn::GetAtt": [ + "AwsApiCallEC2describeVpcs", + "assertion" + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/asset.d47f7e6772bfdf47ecbc070ffe204baf53bacbfbf7814eb407bd8ea108c1c1bb.bundle/index.js b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/asset.d47f7e6772bfdf47ecbc070ffe204baf53bacbfbf7814eb407bd8ea108c1c1bb.bundle/index.js new file mode 100644 index 0000000000000..a9e7e7241efc7 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/asset.d47f7e6772bfdf47ecbc070ffe204baf53bacbfbf7814eb407bd8ea108c1c1bb.bundle/index.js @@ -0,0 +1,669 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// lib/assertions/providers/lambda-handler/index.ts +var lambda_handler_exports = {}; +__export(lambda_handler_exports, { + handler: () => handler +}); +module.exports = __toCommonJS(lambda_handler_exports); + +// ../assertions/lib/matcher.ts +var Matcher = class { + static isMatcher(x) { + return x && x instanceof Matcher; + } +}; +var MatchResult = class { + constructor(target) { + this.failures = []; + this.captures = /* @__PURE__ */ new Map(); + this.finalized = false; + this.target = target; + } + push(matcher, path, message) { + return this.recordFailure({ matcher, path, message }); + } + recordFailure(failure) { + this.failures.push(failure); + return this; + } + hasFailed() { + return this.failures.length !== 0; + } + get failCount() { + return this.failures.length; + } + compose(id, inner) { + const innerF = inner.failures; + this.failures.push(...innerF.map((f) => { + return { path: [id, ...f.path], message: f.message, matcher: f.matcher }; + })); + inner.captures.forEach((vals, capture) => { + vals.forEach((value) => this.recordCapture({ capture, value })); + }); + return this; + } + finished() { + if (this.finalized) { + return this; + } + if (this.failCount === 0) { + this.captures.forEach((vals, cap) => cap._captured.push(...vals)); + } + this.finalized = true; + return this; + } + toHumanStrings() { + return this.failures.map((r) => { + const loc = r.path.length === 0 ? "" : ` at ${r.path.join("")}`; + return "" + r.message + loc + ` (using ${r.matcher.name} matcher)`; + }); + } + recordCapture(options) { + let values = this.captures.get(options.capture); + if (values === void 0) { + values = []; + } + values.push(options.value); + this.captures.set(options.capture, values); + } +}; + +// ../assertions/lib/private/matchers/absent.ts +var AbsentMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual !== void 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Received ${actual}, but key should be absent` + }); + } + return result; + } +}; + +// ../assertions/lib/private/type.ts +function getType(obj) { + return Array.isArray(obj) ? "array" : typeof obj; +} + +// ../assertions/lib/match.ts +var Match = class { + static absent() { + return new AbsentMatch("absent"); + } + static arrayWith(pattern) { + return new ArrayMatch("arrayWith", pattern); + } + static arrayEquals(pattern) { + return new ArrayMatch("arrayEquals", pattern, { subsequence: false }); + } + static exact(pattern) { + return new LiteralMatch("exact", pattern, { partialObjects: false }); + } + static objectLike(pattern) { + return new ObjectMatch("objectLike", pattern); + } + static objectEquals(pattern) { + return new ObjectMatch("objectEquals", pattern, { partial: false }); + } + static not(pattern) { + return new NotMatch("not", pattern); + } + static serializedJson(pattern) { + return new SerializedJson("serializedJson", pattern); + } + static anyValue() { + return new AnyMatch("anyValue"); + } + static stringLikeRegexp(pattern) { + return new StringLikeRegexpMatch("stringLikeRegexp", pattern); + } +}; +var LiteralMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partialObjects = options.partialObjects ?? false; + if (Matcher.isMatcher(this.pattern)) { + throw new Error("LiteralMatch cannot directly contain another matcher. Remove the top-level matcher or nest it more deeply."); + } + } + test(actual) { + if (Array.isArray(this.pattern)) { + return new ArrayMatch(this.name, this.pattern, { subsequence: false, partialObjects: this.partialObjects }).test(actual); + } + if (typeof this.pattern === "object") { + return new ObjectMatch(this.name, this.pattern, { partial: this.partialObjects }).test(actual); + } + const result = new MatchResult(actual); + if (typeof this.pattern !== typeof actual) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected type ${typeof this.pattern} but received ${getType(actual)}` + }); + return result; + } + if (actual !== this.pattern) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected ${this.pattern} but received ${actual}` + }); + } + return result; + } +}; +var ArrayMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.subsequence = options.subsequence ?? true; + this.partialObjects = options.partialObjects ?? false; + } + test(actual) { + if (!Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type array but received ${getType(actual)}` + }); + } + if (!this.subsequence && this.pattern.length !== actual.length) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected array of length ${this.pattern.length} but received ${actual.length}` + }); + } + let patternIdx = 0; + let actualIdx = 0; + const result = new MatchResult(actual); + while (patternIdx < this.pattern.length && actualIdx < actual.length) { + const patternElement = this.pattern[patternIdx]; + const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement, { partialObjects: this.partialObjects }); + const matcherName = matcher.name; + if (this.subsequence && (matcherName == "absent" || matcherName == "anyValue")) { + throw new Error(`The Matcher ${matcherName}() cannot be nested within arrayWith()`); + } + const innerResult = matcher.test(actual[actualIdx]); + if (!this.subsequence || !innerResult.hasFailed()) { + result.compose(`[${actualIdx}]`, innerResult); + patternIdx++; + actualIdx++; + } else { + actualIdx++; + } + } + for (; patternIdx < this.pattern.length; patternIdx++) { + const pattern = this.pattern[patternIdx]; + const element = Matcher.isMatcher(pattern) || typeof pattern === "object" ? " " : ` [${pattern}] `; + result.recordFailure({ + matcher: this, + path: [], + message: `Missing element${element}at pattern index ${patternIdx}` + }); + } + return result; + } +}; +var ObjectMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partial = options.partial ?? true; + } + test(actual) { + if (typeof actual !== "object" || Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type object but received ${getType(actual)}` + }); + } + const result = new MatchResult(actual); + if (!this.partial) { + for (const a of Object.keys(actual)) { + if (!(a in this.pattern)) { + result.recordFailure({ + matcher: this, + path: [`/${a}`], + message: "Unexpected key" + }); + } + } + } + for (const [patternKey, patternVal] of Object.entries(this.pattern)) { + if (!(patternKey in actual) && !(patternVal instanceof AbsentMatch)) { + result.recordFailure({ + matcher: this, + path: [`/${patternKey}`], + message: `Missing key '${patternKey}' among {${Object.keys(actual).join(",")}}` + }); + continue; + } + const matcher = Matcher.isMatcher(patternVal) ? patternVal : new LiteralMatch(this.name, patternVal, { partialObjects: this.partial }); + const inner = matcher.test(actual[patternKey]); + result.compose(`/${patternKey}`, inner); + } + return result; + } +}; +var SerializedJson = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const result = new MatchResult(actual); + if (getType(actual) !== "string") { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected JSON as a string but found ${getType(actual)}` + }); + return result; + } + let parsed; + try { + parsed = JSON.parse(actual); + } catch (err) { + if (err instanceof SyntaxError) { + result.recordFailure({ + matcher: this, + path: [], + message: `Invalid JSON string: ${actual}` + }); + return result; + } else { + throw err; + } + } + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(parsed); + result.compose(`(${this.name})`, innerResult); + return result; + } +}; +var NotMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(actual); + const result = new MatchResult(actual); + if (innerResult.failCount === 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Found unexpected match: ${JSON.stringify(actual, void 0, 2)}` + }); + } + return result; + } +}; +var AnyMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual == null) { + result.recordFailure({ + matcher: this, + path: [], + message: "Expected a value but found none" + }); + } + return result; + } +}; +var StringLikeRegexpMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const result = new MatchResult(actual); + const regex = new RegExp(this.pattern, "gm"); + if (typeof actual !== "string") { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected a string, but got '${typeof actual}'` + }); + } + if (!regex.test(actual)) { + result.recordFailure({ + matcher: this, + path: [], + message: `String '${actual}' did not match pattern '${this.pattern}'` + }); + } + return result; + } +}; + +// lib/assertions/providers/lambda-handler/base.ts +var https = __toESM(require("https")); +var url = __toESM(require("url")); +var CustomResourceHandler = class { + constructor(event, context) { + this.event = event; + this.context = context; + this.timedOut = false; + this.timeout = setTimeout(async () => { + await this.respond({ + status: "FAILED", + reason: "Lambda Function Timeout", + data: this.context.logStreamName + }); + this.timedOut = true; + }, context.getRemainingTimeInMillis() - 1200); + this.event = event; + this.physicalResourceId = extractPhysicalResourceId(event); + } + async handle() { + try { + const response = await this.processEvent(this.event.ResourceProperties); + return response; + } catch (e) { + console.log(e); + throw e; + } finally { + clearTimeout(this.timeout); + } + } + respond(response) { + if (this.timedOut) { + return; + } + const cfResponse = { + Status: response.status, + Reason: response.reason, + PhysicalResourceId: this.physicalResourceId, + StackId: this.event.StackId, + RequestId: this.event.RequestId, + LogicalResourceId: this.event.LogicalResourceId, + NoEcho: false, + Data: response.data + }; + const responseBody = JSON.stringify(cfResponse); + console.log("Responding to CloudFormation", responseBody); + const parsedUrl = url.parse(this.event.ResponseURL); + const requestOptions = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: "PUT", + headers: { "content-type": "", "content-length": responseBody.length } + }; + return new Promise((resolve, reject) => { + try { + const request2 = https.request(requestOptions, resolve); + request2.on("error", reject); + request2.write(responseBody); + request2.end(); + } catch (e) { + reject(e); + } + }); + } +}; +function extractPhysicalResourceId(event) { + switch (event.RequestType) { + case "Create": + return event.LogicalResourceId; + case "Update": + case "Delete": + return event.PhysicalResourceId; + } +} + +// lib/assertions/providers/lambda-handler/assertion.ts +var AssertionHandler = class extends CustomResourceHandler { + async processEvent(request2) { + let actual = decodeCall(request2.actual); + const expected = decodeCall(request2.expected); + let result; + const matcher = new MatchCreator(expected).getMatcher(); + console.log(`Testing equality between ${JSON.stringify(request2.actual)} and ${JSON.stringify(request2.expected)}`); + const matchResult = matcher.test(actual); + matchResult.finished(); + if (matchResult.hasFailed()) { + result = { + failed: true, + assertion: JSON.stringify({ + status: "fail", + message: [ + ...matchResult.toHumanStrings(), + JSON.stringify(matchResult.target, void 0, 2) + ].join("\n") + }) + }; + if (request2.failDeployment) { + throw new Error(result.assertion); + } + } else { + result = { + assertion: JSON.stringify({ + status: "success" + }) + }; + } + return result; + } +}; +var MatchCreator = class { + constructor(obj) { + this.parsedObj = { + matcher: obj + }; + } + getMatcher() { + try { + const final = JSON.parse(JSON.stringify(this.parsedObj), function(_k, v) { + const nested = Object.keys(v)[0]; + switch (nested) { + case "$ArrayWith": + return Match.arrayWith(v[nested]); + case "$ObjectLike": + return Match.objectLike(v[nested]); + case "$StringLike": + return Match.stringLikeRegexp(v[nested]); + default: + return v; + } + }); + if (Matcher.isMatcher(final.matcher)) { + return final.matcher; + } + return Match.exact(final.matcher); + } catch { + return Match.exact(this.parsedObj.matcher); + } + } +}; +function decodeCall(call) { + if (!call) { + return void 0; + } + try { + const parsed = JSON.parse(call); + return parsed; + } catch (e) { + return call; + } +} + +// lib/assertions/providers/lambda-handler/utils.ts +function decode(object) { + return JSON.parse(JSON.stringify(object), (_k, v) => { + switch (v) { + case "TRUE:BOOLEAN": + return true; + case "FALSE:BOOLEAN": + return false; + default: + return v; + } + }); +} + +// lib/assertions/providers/lambda-handler/sdk.ts +function flatten(object) { + return Object.assign( + {}, + ...function _flatten(child, path = []) { + return [].concat(...Object.keys(child).map((key) => { + let childKey = Buffer.isBuffer(child[key]) ? child[key].toString("utf8") : child[key]; + if (typeof childKey === "string") { + childKey = isJsonString(childKey); + } + return typeof childKey === "object" && childKey !== null ? _flatten(childKey, path.concat([key])) : { [path.concat([key]).join(".")]: childKey }; + })); + }(object) + ); +} +var AwsApiCallHandler = class extends CustomResourceHandler { + async processEvent(request2) { + const AWS = require("aws-sdk"); + console.log(`AWS SDK VERSION: ${AWS.VERSION}`); + if (!Object.prototype.hasOwnProperty.call(AWS, request2.service)) { + throw Error(`Service ${request2.service} does not exist in AWS SDK version ${AWS.VERSION}.`); + } + const service = new AWS[request2.service](); + const response = await service[request2.api](request2.parameters && decode(request2.parameters)).promise(); + console.log(`SDK response received ${JSON.stringify(response)}`); + delete response.ResponseMetadata; + const respond = { + apiCallResponse: response + }; + const flatData = { + ...flatten(respond) + }; + const resp = request2.flattenResponse === "true" ? flatData : respond; + console.log(`Returning result ${JSON.stringify(resp)}`); + return resp; + } +}; +function isJsonString(value) { + try { + return JSON.parse(value); + } catch { + return value; + } +} + +// lib/assertions/providers/lambda-handler/types.ts +var ASSERT_RESOURCE_TYPE = "Custom::DeployAssert@AssertEquals"; +var SDK_RESOURCE_TYPE_PREFIX = "Custom::DeployAssert@SdkCall"; + +// lib/assertions/providers/lambda-handler/index.ts +async function handler(event, context) { + console.log(`Event: ${JSON.stringify({ ...event, ResponseURL: "..." })}`); + const provider = createResourceHandler(event, context); + try { + if (event.RequestType === "Delete") { + await provider.respond({ + status: "SUCCESS", + reason: "OK" + }); + return; + } + const result = await provider.handle(); + const actualPath = event.ResourceProperties.actualPath; + const actual = actualPath ? result[`apiCallResponse.${actualPath}`] : result.apiCallResponse; + if ("expected" in event.ResourceProperties) { + const assertion = new AssertionHandler({ + ...event, + ResourceProperties: { + ServiceToken: event.ServiceToken, + actual, + expected: event.ResourceProperties.expected + } + }, context); + try { + const assertionResult = await assertion.handle(); + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: { + ...assertionResult, + ...result + } + }); + return; + } catch (e) { + await provider.respond({ + status: "FAILED", + reason: e.message ?? "Internal Error" + }); + return; + } + } + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: result + }); + } catch (e) { + await provider.respond({ + status: "FAILED", + reason: e.message ?? "Internal Error" + }); + return; + } + return; +} +function createResourceHandler(event, context) { + if (event.ResourceType.startsWith(SDK_RESOURCE_TYPE_PREFIX)) { + return new AwsApiCallHandler(event, context); + } else if (event.ResourceType.startsWith(ASSERT_RESOURCE_TYPE)) { + return new AssertionHandler(event, context); + } else { + throw new Error(`Unsupported resource type "${event.ResourceType}`); + } +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + handler +}); diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/aws-cdk-ec2-ipam-vpc.assets.json b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/aws-cdk-ec2-ipam-vpc.assets.json new file mode 100644 index 0000000000000..bec195ba38184 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/aws-cdk-ec2-ipam-vpc.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "5b3a6377cf636bdf8b57c28c24c57742dfd668f4536432348222f40b5ce1a63d": { + "source": { + "path": "aws-cdk-ec2-ipam-vpc.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "5b3a6377cf636bdf8b57c28c24c57742dfd668f4536432348222f40b5ce1a63d.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/aws-cdk-ec2-ipam-vpc.template.json b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/aws-cdk-ec2-ipam-vpc.template.json new file mode 100644 index 0000000000000..82a71e9a08722 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/aws-cdk-ec2-ipam-vpc.template.json @@ -0,0 +1,261 @@ +{ + "Resources": { + "IPAM": { + "Type": "AWS::EC2::IPAM", + "Properties": { + "OperatingRegions": [ + { + "RegionName": { + "Ref": "AWS::Region" + } + } + ], + "Tags": [ + { + "Key": "stack", + "Value": { + "Ref": "AWS::StackId" + } + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "Pool": { + "Type": "AWS::EC2::IPAMPool", + "Properties": { + "AddressFamily": "ipv4", + "IpamScopeId": { + "Fn::GetAtt": [ + "IPAM", + "PrivateDefaultScopeId" + ] + }, + "AutoImport": false, + "Description": "Testing pool", + "Locale": { + "Ref": "AWS::Region" + }, + "ProvisionedCidrs": [ + { + "Cidr": "100.100.0.0/16" + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "AwsIpamVpcD3A1DAEE": { + "Type": "AWS::EC2::VPC", + "Properties": { + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Ipv4IpamPoolId": { + "Ref": "Pool" + }, + "Ipv4NetmaskLength": 18, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc" + } + ] + } + }, + "AwsIpamVpcprivateSubnet1Subnet0AC8649F": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "AwsIpamVpcD3A1DAEE" + }, + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": { + "Fn::Select": [ + 0, + { + "Fn::Cidr": [ + { + "Fn::GetAtt": [ + "AwsIpamVpcD3A1DAEE", + "CidrBlock" + ] + }, + 64, + "8" + ] + } + ] + }, + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet1" + } + ] + } + }, + "AwsIpamVpcprivateSubnet1RouteTable2A97E440": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "AwsIpamVpcD3A1DAEE" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet1" + } + ] + } + }, + "AwsIpamVpcprivateSubnet1RouteTableAssociationE7D2E570": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "AwsIpamVpcprivateSubnet1RouteTable2A97E440" + }, + "SubnetId": { + "Ref": "AwsIpamVpcprivateSubnet1Subnet0AC8649F" + } + } + }, + "AwsIpamVpcprivateSubnet2Subnet577660DE": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "AwsIpamVpcD3A1DAEE" + }, + "AvailabilityZone": { + "Fn::Select": [ + 1, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": { + "Fn::Select": [ + 1, + { + "Fn::Cidr": [ + { + "Fn::GetAtt": [ + "AwsIpamVpcD3A1DAEE", + "CidrBlock" + ] + }, + 64, + "8" + ] + } + ] + }, + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet2" + } + ] + } + }, + "AwsIpamVpcprivateSubnet2RouteTableDDE2D1BF": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "AwsIpamVpcD3A1DAEE" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet2" + } + ] + } + }, + "AwsIpamVpcprivateSubnet2RouteTableAssociation52A3C85A": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "AwsIpamVpcprivateSubnet2RouteTableDDE2D1BF" + }, + "SubnetId": { + "Ref": "AwsIpamVpcprivateSubnet2Subnet577660DE" + } + } + } + }, + "Outputs": { + "ExportsOutputFnGetAttAwsIpamVpcD3A1DAEEVpcId808CC597": { + "Value": { + "Fn::GetAtt": [ + "AwsIpamVpcD3A1DAEE", + "VpcId" + ] + }, + "Export": { + "Name": "aws-cdk-ec2-ipam-vpc:ExportsOutputFnGetAttAwsIpamVpcD3A1DAEEVpcId808CC597" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/integ.json b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/integ.json new file mode 100644 index 0000000000000..158faa819fea8 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/integ.json @@ -0,0 +1,15 @@ +{ + "version": "21.0.0", + "testCases": { + "Vpc-Ipam/DefaultTest": { + "stacks": [ + "aws-cdk-ec2-ipam-vpc" + ], + "allowDestroy": [ + "EC2::IPAM" + ], + "assertionStack": "Vpc-Ipam/DefaultTest/DeployAssert", + "assertionStackName": "VpcIpamDefaultTestDeployAssertB1CA1C3A" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..98824f54ae317 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/manifest.json @@ -0,0 +1,190 @@ +{ + "version": "21.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "aws-cdk-ec2-ipam-vpc.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-ec2-ipam-vpc.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-ec2-ipam-vpc": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-ec2-ipam-vpc.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/5b3a6377cf636bdf8b57c28c24c57742dfd668f4536432348222f40b5ce1a63d.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-ec2-ipam-vpc.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-ec2-ipam-vpc.assets" + ], + "metadata": { + "/aws-cdk-ec2-ipam-vpc/IPAM": [ + { + "type": "aws:cdk:logicalId", + "data": "IPAM" + } + ], + "/aws-cdk-ec2-ipam-vpc/Pool": [ + { + "type": "aws:cdk:logicalId", + "data": "Pool" + } + ], + "/aws-cdk-ec2-ipam-vpc/AwsIpamVpc/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsIpamVpcD3A1DAEE" + } + ], + "/aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet1/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsIpamVpcprivateSubnet1Subnet0AC8649F" + } + ], + "/aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet1/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsIpamVpcprivateSubnet1RouteTable2A97E440" + } + ], + "/aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet1/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsIpamVpcprivateSubnet1RouteTableAssociationE7D2E570" + } + ], + "/aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet2/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsIpamVpcprivateSubnet2Subnet577660DE" + } + ], + "/aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet2/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsIpamVpcprivateSubnet2RouteTableDDE2D1BF" + } + ], + "/aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet2/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsIpamVpcprivateSubnet2RouteTableAssociation52A3C85A" + } + ], + "/aws-cdk-ec2-ipam-vpc/Exports/Output{\"Fn::GetAtt\":[\"AwsIpamVpcD3A1DAEE\",\"VpcId\"]}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputFnGetAttAwsIpamVpcD3A1DAEEVpcId808CC597" + } + ], + "/aws-cdk-ec2-ipam-vpc/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-ec2-ipam-vpc/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-ec2-ipam-vpc" + }, + "VpcIpamDefaultTestDeployAssertB1CA1C3A.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "VpcIpamDefaultTestDeployAssertB1CA1C3A.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "VpcIpamDefaultTestDeployAssertB1CA1C3A": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "VpcIpamDefaultTestDeployAssertB1CA1C3A.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/5ee44ec2182f45d2de6a0f0dce2ba54c61792d88196caa30d00380d2a5bb40c2.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "VpcIpamDefaultTestDeployAssertB1CA1C3A.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-ec2-ipam-vpc", + "VpcIpamDefaultTestDeployAssertB1CA1C3A.assets" + ], + "metadata": { + "/Vpc-Ipam/DefaultTest/DeployAssert/AwsApiCallEC2describeVpcs/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsApiCallEC2describeVpcs" + } + ], + "/Vpc-Ipam/DefaultTest/DeployAssert/AwsApiCallEC2describeVpcs/AssertionResults": [ + { + "type": "aws:cdk:logicalId", + "data": "AssertionResultsAwsApiCallEC2describeVpcs" + } + ], + "/Vpc-Ipam/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73" + } + ], + "/Vpc-Ipam/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F" + } + ], + "/Vpc-Ipam/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/Vpc-Ipam/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "Vpc-Ipam/DefaultTest/DeployAssert" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/tree.json b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/tree.json new file mode 100644 index 0000000000000..a2e180fba34fa --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/vpc-ipam.integ.snapshot/tree.json @@ -0,0 +1,499 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.123" + } + }, + "aws-cdk-ec2-ipam-vpc": { + "id": "aws-cdk-ec2-ipam-vpc", + "path": "aws-cdk-ec2-ipam-vpc", + "children": { + "IPAM": { + "id": "IPAM", + "path": "aws-cdk-ec2-ipam-vpc/IPAM", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::IPAM", + "aws:cdk:cloudformation:props": { + "operatingRegions": [ + { + "regionName": { + "Ref": "AWS::Region" + } + } + ], + "tags": [ + { + "key": "stack", + "value": { + "Ref": "AWS::StackId" + } + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Pool": { + "id": "Pool", + "path": "aws-cdk-ec2-ipam-vpc/Pool", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::IPAMPool", + "aws:cdk:cloudformation:props": { + "addressFamily": "ipv4", + "ipamScopeId": { + "Fn::GetAtt": [ + "IPAM", + "PrivateDefaultScopeId" + ] + }, + "autoImport": false, + "description": "Testing pool", + "locale": { + "Ref": "AWS::Region" + }, + "provisionedCidrs": [ + { + "cidr": "100.100.0.0/16" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "AwsIpamVpc": { + "id": "AwsIpamVpc", + "path": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::VPC", + "aws:cdk:cloudformation:props": { + "enableDnsHostnames": true, + "enableDnsSupport": true, + "instanceTenancy": "default", + "ipv4IpamPoolId": { + "Ref": "Pool" + }, + "ipv4NetmaskLength": 18, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "privateSubnet1": { + "id": "privateSubnet1", + "path": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet1", + "children": { + "Subnet": { + "id": "Subnet", + "path": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet1/Subnet", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "AwsIpamVpcD3A1DAEE" + }, + "availabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "cidrBlock": { + "Fn::Select": [ + 0, + { + "Fn::Cidr": [ + { + "Fn::GetAtt": [ + "AwsIpamVpcD3A1DAEE", + "CidrBlock" + ] + }, + 64, + "8" + ] + } + ] + }, + "mapPublicIpOnLaunch": false, + "tags": [ + { + "key": "aws-cdk:subnet-name", + "value": "private" + }, + { + "key": "aws-cdk:subnet-type", + "value": "Isolated" + }, + { + "key": "Name", + "value": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Acl": { + "id": "Acl", + "path": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet1/Acl", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "RouteTable": { + "id": "RouteTable", + "path": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet1/RouteTable", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "AwsIpamVpcD3A1DAEE" + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "RouteTableAssociation": { + "id": "RouteTableAssociation", + "path": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet1/RouteTableAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SubnetRouteTableAssociation", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "AwsIpamVpcprivateSubnet1RouteTable2A97E440" + }, + "subnetId": { + "Ref": "AwsIpamVpcprivateSubnet1Subnet0AC8649F" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "privateSubnet2": { + "id": "privateSubnet2", + "path": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet2", + "children": { + "Subnet": { + "id": "Subnet", + "path": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet2/Subnet", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "AwsIpamVpcD3A1DAEE" + }, + "availabilityZone": { + "Fn::Select": [ + 1, + { + "Fn::GetAZs": "" + } + ] + }, + "cidrBlock": { + "Fn::Select": [ + 1, + { + "Fn::Cidr": [ + { + "Fn::GetAtt": [ + "AwsIpamVpcD3A1DAEE", + "CidrBlock" + ] + }, + 64, + "8" + ] + } + ] + }, + "mapPublicIpOnLaunch": false, + "tags": [ + { + "key": "aws-cdk:subnet-name", + "value": "private" + }, + { + "key": "aws-cdk:subnet-type", + "value": "Isolated" + }, + { + "key": "Name", + "value": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Acl": { + "id": "Acl", + "path": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet2/Acl", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "RouteTable": { + "id": "RouteTable", + "path": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet2/RouteTable", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "AwsIpamVpcD3A1DAEE" + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "RouteTableAssociation": { + "id": "RouteTableAssociation", + "path": "aws-cdk-ec2-ipam-vpc/AwsIpamVpc/privateSubnet2/RouteTableAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SubnetRouteTableAssociation", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "AwsIpamVpcprivateSubnet2RouteTableDDE2D1BF" + }, + "subnetId": { + "Ref": "AwsIpamVpcprivateSubnet2Subnet577660DE" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Exports": { + "id": "Exports", + "path": "aws-cdk-ec2-ipam-vpc/Exports", + "children": { + "Output{\"Fn::GetAtt\":[\"AwsIpamVpcD3A1DAEE\",\"VpcId\"]}": { + "id": "Output{\"Fn::GetAtt\":[\"AwsIpamVpcD3A1DAEE\",\"VpcId\"]}", + "path": "aws-cdk-ec2-ipam-vpc/Exports/Output{\"Fn::GetAtt\":[\"AwsIpamVpcD3A1DAEE\",\"VpcId\"]}", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.123" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "Vpc-Ipam": { + "id": "Vpc-Ipam", + "path": "Vpc-Ipam", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "Vpc-Ipam/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "Vpc-Ipam/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.123" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "Vpc-Ipam/DefaultTest/DeployAssert", + "children": { + "AwsApiCallEC2describeVpcs": { + "id": "AwsApiCallEC2describeVpcs", + "path": "Vpc-Ipam/DefaultTest/DeployAssert/AwsApiCallEC2describeVpcs", + "children": { + "SdkProvider": { + "id": "SdkProvider", + "path": "Vpc-Ipam/DefaultTest/DeployAssert/AwsApiCallEC2describeVpcs/SdkProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "Vpc-Ipam/DefaultTest/DeployAssert/AwsApiCallEC2describeVpcs/SdkProvider/AssertionsProvider", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.123" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "Vpc-Ipam/DefaultTest/DeployAssert/AwsApiCallEC2describeVpcs/Default", + "children": { + "Default": { + "id": "Default", + "path": "Vpc-Ipam/DefaultTest/DeployAssert/AwsApiCallEC2describeVpcs/Default/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + }, + "AssertionResults": { + "id": "AssertionResults", + "path": "Vpc-Ipam/DefaultTest/DeployAssert/AwsApiCallEC2describeVpcs/AssertionResults", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AwsApiCall", + "version": "0.0.0" + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81": { + "id": "SingletonFunction1488541a7b23466481b69b4408076b81", + "path": "Vpc-Ipam/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81", + "children": { + "Staging": { + "id": "Staging", + "path": "Vpc-Ipam/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "Vpc-Ipam/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "Vpc-Ipam/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.123" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts index e5ae1a6ed00cc..e350b1a45b0c8 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts @@ -26,6 +26,7 @@ import { SubnetType, TrafficDirection, Vpc, + IpAddresses, } from '../lib'; describe('vpc', () => { @@ -174,7 +175,7 @@ describe('vpc', () => { test('with all of the properties set, it successfully sets the correct VPC properties', () => { const stack = getTestStack(); new Vpc(stack, 'TheVPC', { - cidr: '192.168.0.0/16', + ipAddresses: IpAddresses.cidr('192.168.0.0/16'), enableDnsHostnames: false, enableDnsSupport: false, defaultInstanceTenancy: DefaultInstanceTenancy.DEDICATED, @@ -205,7 +206,7 @@ describe('vpc', () => { const stack = getTestStack(); const vpc = new Vpc(stack, 'TheVPC', { - cidr: '192.168.0.0/16', + ipAddresses: IpAddresses.cidr('192.168.0.0/16'), enableDnsHostnames: input.dnsHostnames, enableDnsSupport: input.dnsSupport, defaultInstanceTenancy: DefaultInstanceTenancy.DEDICATED, @@ -362,7 +363,7 @@ describe('vpc', () => { test('with subnets and reserved subnets defined, VPC subnet count should not contain reserved subnets ', () => { const stack = getTestStack(); new Vpc(stack, 'TheVPC', { - cidr: '10.0.0.0/16', + ipAddresses: IpAddresses.cidr('10.0.0.0/16'), subnetConfiguration: [ { cidrMask: 24, @@ -389,7 +390,7 @@ describe('vpc', () => { test('with reserved subnets, any other subnets should not have cidrBlock from within reserved space', () => { const stack = getTestStack(); new Vpc(stack, 'TheVPC', { - cidr: '10.0.0.0/16', + ipAddresses: IpAddresses.cidr('10.0.0.0/16'), subnetConfiguration: [ { cidrMask: 24, @@ -410,19 +411,22 @@ describe('vpc', () => { ], maxAzs: 3, }); + + const template = Template.fromStack(stack); + for (let i = 0; i < 3; i++) { - Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { + template.hasResourceProperties('AWS::EC2::Subnet', { CidrBlock: `10.0.${i}.0/24`, }); } for (let i = 3; i < 6; i++) { - const matchingSubnets = Template.fromStack(stack).findResources('AWS::EC2::Subnet', { + const matchingSubnets = template.findResources('AWS::EC2::Subnet', { CidrBlock: `10.0.${i}.0/24`, }); expect(Object.keys(matchingSubnets).length).toBe(0); } for (let i = 6; i < 9; i++) { - Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { + template.hasResourceProperties('AWS::EC2::Subnet', { CidrBlock: `10.0.${i}.0/24`, }); } @@ -432,7 +436,7 @@ describe('vpc', () => { const stack = getTestStack(); const zones = stack.availabilityZones.length; new Vpc(stack, 'TheVPC', { - cidr: '10.0.0.0/21', + ipAddresses: IpAddresses.cidr('10.0.0.0/21'), subnetConfiguration: [ { cidrMask: 24, @@ -470,7 +474,7 @@ describe('vpc', () => { test('with custom subnets and natGateways = 2 there should be only two NATGW', () => { const stack = getTestStack(); new Vpc(stack, 'TheVPC', { - cidr: '10.0.0.0/21', + ipAddresses: IpAddresses.cidr('10.0.0.0/21'), natGateways: 2, subnetConfiguration: [ { @@ -842,7 +846,7 @@ describe('vpc', () => { test('natGateways = 0 allows RESERVED PRIVATE subnets', () => { const stack = getTestStack(); new Vpc(stack, 'VPC', { - cidr: '10.0.0.0/16', + ipAddresses: IpAddresses.cidr('10.0.0.0/16'), subnetConfiguration: [ { name: 'ingress', @@ -866,7 +870,7 @@ describe('vpc', () => { test('EIP passed with NAT gateway does not create duplicate EIP', () => { const stack = getTestStack(); new Vpc(stack, 'VPC', { - cidr: '10.0.0.0/16', + ipAddresses: IpAddresses.cidr('10.0.0.0/16'), subnetConfiguration: [ { cidrMask: 24, @@ -1128,7 +1132,7 @@ describe('vpc', () => { const stack = new Stack(); expect(() => { new Vpc(stack, 'Vpc', { - cidr: Lazy.string({ produce: () => 'abc' }), + ipAddresses: IpAddresses.cidr(Lazy.string({ produce: () => 'abc' })), }); }).toThrow(/property must be a concrete CIDR string/); }); @@ -1450,7 +1454,7 @@ describe('vpc', () => { const stack = getTestStack(); // WHEN - const vpc = new Vpc(stack, 'TheVPC', { cidr: '192.168.0.0/16' }); + const vpc = new Vpc(stack, 'TheVPC', { ipAddresses: IpAddresses.cidr('192.168.0.0/16') }); new CfnOutput(stack, 'Output', { value: (vpc.publicSubnets[0] as Subnet).subnetNetworkAclAssociationId, }); @@ -1467,7 +1471,7 @@ describe('vpc', () => { test('if ACL is replaced new ACL reference is returned', () => { // GIVEN const stack = getTestStack(); - const vpc = new Vpc(stack, 'TheVPC', { cidr: '192.168.0.0/16' }); + const vpc = new Vpc(stack, 'TheVPC', { ipAddresses: IpAddresses.cidr('192.168.0.0/16') }); // WHEN new CfnOutput(stack, 'Output', { @@ -1491,7 +1495,7 @@ describe('vpc', () => { describe('When creating a VPC with a custom CIDR range', () => { test('vpc.vpcCidrBlock is the correct network range', () => { const stack = getTestStack(); - new Vpc(stack, 'TheVPC', { cidr: '192.168.0.0/16' }); + new Vpc(stack, 'TheVPC', { ipAddresses: IpAddresses.cidr('192.168.0.0/16') }); Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', { CidrBlock: '192.168.0.0/16', }); @@ -1930,7 +1934,7 @@ describe('vpc', () => { // IP space is split into 6 pieces, one public/one private per AZ const vpc = new Vpc(stack, 'VPC', { - cidr: '10.0.0.0/16', + ipAddresses: IpAddresses.cidr('10.0.0.0/16'), maxAzs: 3, }); @@ -1961,7 +1965,7 @@ describe('vpc', () => { // IP space is split into 6 pieces, one public/one private per AZ const vpc = new Vpc(stack, 'VPC', { - cidr: '10.0.0.0/16', + ipAddresses: IpAddresses.cidr('10.0.0.0/16'), maxAzs: 3, });