From e7125f3ecd57da76be1b5b5f4914a9513c5b55f5 Mon Sep 17 00:00:00 2001 From: shikha372 Date: Wed, 22 Jan 2025 23:43:02 -0800 Subject: [PATCH] chore(vpcv2): increasing test coverage for graduation fixing tests --- packages/@aws-cdk/aws-ec2-alpha/README.md | 38 ++++++ .../@aws-cdk/aws-ec2-alpha/jest.config.js | 6 - packages/@aws-cdk/aws-ec2-alpha/lib/ipam.ts | 2 +- packages/@aws-cdk/aws-ec2-alpha/lib/route.ts | 12 +- .../@aws-cdk/aws-ec2-alpha/lib/vpc-v2-base.ts | 38 ++++-- .../@aws-cdk/aws-ec2-alpha/test/ipam.test.ts | 105 +++++++++++++++++ .../@aws-cdk/aws-ec2-alpha/test/route.test.ts | 111 +++++++++++++++++- .../test/{util.ts => subnet-test-util.ts} | 4 +- .../aws-ec2-alpha/test/subnet-v2.test.ts | 83 ++++++++++++- .../@aws-cdk/aws-ec2-alpha/test/util.test.ts | 27 ++++- .../aws-ec2-alpha/test/vpc-add-method.test.ts | 109 ++++++++++++++++- .../aws-ec2-alpha/test/vpc-v2.test.ts | 62 ++++++++++ .../aws-ec2-alpha/test/vpcv2-import.test.ts | 96 +++++++++++++++ 13 files changed, 662 insertions(+), 31 deletions(-) rename packages/@aws-cdk/aws-ec2-alpha/test/{util.ts => subnet-test-util.ts} (85%) diff --git a/packages/@aws-cdk/aws-ec2-alpha/README.md b/packages/@aws-cdk/aws-ec2-alpha/README.md index b37359349caa6..c1922e6f76e5b 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/README.md +++ b/packages/@aws-cdk/aws-ec2-alpha/README.md @@ -41,6 +41,7 @@ new VpcV2(this, 'Vpc', { `SubnetV2` is a re-write of the [`ec2.Subnet`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Subnet.html) construct. This new construct can be used to add subnets to a `VpcV2` instance: +Note: When defining a subnet with `SubnetV2`, CDK automatically creates a new route table, unless a route table is explicitly provided as an input to the construct. ```ts @@ -66,6 +67,14 @@ By default `VpcV2` uses `10.0.0.0/16` as the primary CIDR if none is defined. Additional CIDRs can be adding to the VPC via the `secondaryAddressBlocks` prop. The following example illustrates the different options of defining the address blocks: +Note: There’s currently an issue with IPAM pool deletion that may affect the `cdk --destroy` command. This is because IPAM takes time to detect when the IP address pool has been deallocated after the VPC is deleted. The current workaround is to wait until the IP address is fully deallocated from the pool before retrying the deletion. Below command can be used to check allocations for a pool using CLI + +```shell +aws ec2 get-ipam-pool-allocations --ipam-pool-id +``` + +Ref: https://docs.aws.amazon.com/cli/latest/reference/ec2/get-ipam-pool-allocations.html + ```ts const stack = new Stack(); @@ -527,6 +536,7 @@ For more information, see [Enable VPC internet access using internet gateways](h You can add an internet gateway to a VPC using `addInternetGateway` method. By default, this method creates a route in all Public Subnets with outbound destination set to `0.0.0.0` for IPv4 and `::0` for IPv6 enabled VPC. Instead of using the default settings, you can configure a custom destination range by providing an optional input `destination` to the method. +In addition to the custom IP range, you can also choose to filter subnets where default routes should be created. The code example below shows how to add an internet gateway with a custom outbound destination IP range: @@ -546,6 +556,34 @@ myVpc.addInternetGateway({ }); ``` +The following code examples demonstrates how to add an internet gateway with a custom outbound destination IP range for specific subnets: + +```ts +const stack = new Stack(); +const myVpc = new VpcV2(this, 'Vpc'); + +const mySubnet = new SubnetV2(this, 'Subnet', { + vpc: myVpc, + availabilityZone: 'eu-west-2a', + ipv4CidrBlock: new IpCidr('10.0.0.0/24'), + subnetType: SubnetType.PUBLIC }); + +myVpc.addInternetGateway({ + ipv4Destination: '192.168.0.0/16', + subnets: [mySubnet], +}); +``` + +```ts +const stack = new Stack(); +const myVpc = new VpcV2(this, 'Vpc'); + +myVpc.addInternetGateway({ + ipv4Destination: '192.168.0.0/16', + subnets: [{subnetType: SubnetType.PRIVATE_WITH_EGRESS}], +}); +``` + ## Importing an existing VPC You can import an existing VPC and its subnets using the `VpcV2.fromVpcV2Attributes()` method or an individual subnet using `SubnetV2.fromSubnetV2Attributes()` method. diff --git a/packages/@aws-cdk/aws-ec2-alpha/jest.config.js b/packages/@aws-cdk/aws-ec2-alpha/jest.config.js index 4995208baf512..18ed6f0effe60 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/jest.config.js +++ b/packages/@aws-cdk/aws-ec2-alpha/jest.config.js @@ -1,10 +1,4 @@ const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); module.exports = { ...baseConfig, - coverageThreshold: { - global: { - statements: 75, - branches: 63, - }, - }, };; diff --git a/packages/@aws-cdk/aws-ec2-alpha/lib/ipam.ts b/packages/@aws-cdk/aws-ec2-alpha/lib/ipam.ts index 378e9f9726291..e8b2e87076839 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/lib/ipam.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/lib/ipam.ts @@ -514,7 +514,7 @@ export class Ipam extends Resource { if (props?.ipamName) { Tags.of(this).add(NAME_TAG, props.ipamName); } - if (!props?.operatingRegion && !Stack.of(this).region) { + if (props?.operatingRegion && (props.operatingRegion.length === 0)) { throw new Error('Please provide at least one operating region'); } diff --git a/packages/@aws-cdk/aws-ec2-alpha/lib/route.ts b/packages/@aws-cdk/aws-ec2-alpha/lib/route.ts index b6f9eb1ee12e4..6967eb52fc58d 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/lib/route.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/lib/route.ts @@ -608,12 +608,8 @@ export class RouteTargetType { readonly endpoint?: IVpcEndpoint; constructor(props: RouteTargetProps) { - if ((props.gateway && props.endpoint) || (!props.gateway && !props.endpoint)) { - throw new Error('Exactly one of `gateway` or `endpoint` must be specified.'); - } else { - this.gateway = props.gateway; - this.endpoint = props.endpoint; - } + this.gateway = props.gateway; + this.endpoint = props.endpoint; } } @@ -732,6 +728,10 @@ export class Route extends Resource implements IRouteV2 { if (this.target.gateway?.routerType === RouterType.EGRESS_ONLY_INTERNET_GATEWAY && isDestinationIpv4) { throw new Error('Egress only internet gateway does not support IPv4 routing'); } + + if ((props.target.gateway && props.target.endpoint) || (!props.target.gateway && !props.target.endpoint)) { + throw new Error('Exactly one of `gateway` or `endpoint` must be specified.'); + } this.targetRouterType = this.target.gateway ? this.target.gateway.routerType : RouterType.VPC_ENDPOINT; // Gateway generates route automatically via its RouteTable, thus we don't need to generate the resource for it if (!(this.target.endpoint instanceof GatewayVpcEndpoint)) { diff --git a/packages/@aws-cdk/aws-ec2-alpha/lib/vpc-v2-base.ts b/packages/@aws-cdk/aws-ec2-alpha/lib/vpc-v2-base.ts index c714ce351de68..27c19cf8ae55b 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/lib/vpc-v2-base.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/lib/vpc-v2-base.ts @@ -60,6 +60,13 @@ export interface InternetGatewayOptions{ * @default - provisioned without a resource name */ readonly internetGatewayName?: string; + + /** + * List of subnets where route to IGW will be added + * + * @default - route created for all subnets with Type `SubnetType.Public` + */ + readonly subnets?: SubnetSelection[]; } /** @@ -438,9 +445,14 @@ export abstract class VpcV2Base extends Resource implements IVpcV2 { }; if (options?.subnets) { + // Use Set to ensure unique subnets + const processedSubnets = new Set(); const subnets = flatten(options.subnets.map(s => this.selectSubnets(s).subnets)); subnets.forEach((subnet) => { - this.createEgressRoute(subnet, egw, options.destination); + if (!processedSubnets.has(subnet.node.id)) { + this.createEgressRoute(subnet, egw, options.destination); + processedSubnets.add(subnet.node.id); + } }); } } @@ -477,9 +489,23 @@ export abstract class VpcV2Base extends Resource implements IVpcV2 { this._internetConnectivityEstablished.add(igw); this._internetGatewayId = igw.routerTargetId; - // If there are no public subnets defined, no default route will be added - if (this.publicSubnets) { - this.publicSubnets.forEach( (s) => this.addDefaultInternetRoute(s, igw, options)); + // Add routes for subnets defined as an input + if (options?.subnets) { + // Use Set to ensure unique subnets + const processedSubnets = new Set(); + const subnets = flatten(options.subnets.map(s => this.selectSubnets(s).subnets)); + subnets.forEach((subnet) => { + if (!processedSubnets.has(subnet.node.id)) { + if (!this.publicSubnets.includes(subnet)) { + Annotations.of(this).addWarningV2('InternetGatewayWarning', + `Subnet ${subnet.node.id} is not a public subnet. Internet Gateway should be added only to public subnets.`); + } + this.addDefaultInternetRoute(subnet, igw, options); + processedSubnets.add(subnet.node.id); + }; + }); // If there are no input subnets defined, default route will be added to all public subnets + } else if (!options?.subnets && this.publicSubnets) { + this.publicSubnets.forEach((publicSubnets) => this.addDefaultInternetRoute(publicSubnets, igw, options)); } } @@ -489,10 +515,6 @@ export abstract class VpcV2Base extends Resource implements IVpcV2 { */ private addDefaultInternetRoute(subnet: ISubnetV2, igw: InternetGateway, options?: InternetGatewayOptions): void { - if (subnet.subnetType !== SubnetType.PUBLIC) { - throw new Error('No public subnets defined to add route for internet gateway'); - } - // Add default route to IGW for IPv6 if (subnet.ipv6CidrBlock) { new Route(this, `${subnet.node.id}-DefaultIPv6Route`, { diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/ipam.test.ts b/packages/@aws-cdk/aws-ec2-alpha/test/ipam.test.ts index 78e681c5dc0f5..922d5c6b9a595 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/ipam.test.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/ipam.test.ts @@ -155,4 +155,109 @@ describe('IPAM Test', () => { ); }); + test('IPAM throws error if awsService is not provided for IPv6 address', () => { + // Create IPAM resources + const ipamRegion = new Ipam(stack, 'TestIpam', { + operatingRegion: ['us-west-2'], + }); + const poolOptions: vpc.PoolOptions = { + addressFamily: AddressFamily.IP_V6, + publicIpSource: IpamPoolPublicIpSource.AMAZON, + locale: 'us-west-2', + }; + expect(() => ipamRegion.publicScope.addPool('TestPool', poolOptions)).toThrow('awsService is required when addressFamily is set to ipv6'); + }); + + test('IPAM throws error if operating region is provided as an empty array', () => { + const app = new cdk.App(); + const stack_new = new cdk.Stack(app, 'TestStack'); + expect(() => new Ipam(stack_new, 'TestIpam', { + operatingRegion: [], + })).toThrow('Please provide at least one operating region'); + }); + + test('IPAM infers region from provided operating region correctly', () => { + const app = new cdk.App(); + const stack_new = new cdk.Stack(app, 'TestStack'); + new Ipam(stack_new, 'TestIpam', { + operatingRegion: ['us-west-2'], + }); + Template.fromStack(stack_new).hasResourceProperties( + 'AWS::EC2::IPAM', { + OperatingRegions: [ + { + RegionName: 'us-west-2', + }, + ], + }, + ); + }); + + test('IPAM infers region from stack if not provided under IPAM class object', () => { + const app = new cdk.App(); + const stack_new = new cdk.Stack(app, 'TestStack', { + env: { + region: 'us-west-2', + }, + }); + new Ipam(stack_new, 'TestIpam', {}); + Template.fromStack(stack_new).hasResourceProperties( + 'AWS::EC2::IPAM', { + OperatingRegions: [ + { + RegionName: 'us-west-2', + }, + ], + }, + ); + }); + + test('IPAM refers to stack region token', () => { + const app = new cdk.App(); + const stack_new = new cdk.Stack(app, 'TestStack'); + new Ipam(stack_new, 'TestIpam', {}); + Template.fromStack(stack_new).hasResourceProperties( + 'AWS::EC2::IPAM', { + OperatingRegions: [ + { + RegionName: { + Ref: 'AWS::Region', + }, + }, + ], + }, + ); + }); + + test('IPAM throws error if locale(region) of pool is not one of the operating regions', () => { + const ipamRegion = new Ipam(stack, 'TestIpam', { + operatingRegion: ['us-west-2'], + }); + const poolOptions: vpc.PoolOptions = { + addressFamily: AddressFamily.IP_V6, + awsService: vpc.AwsServiceName.EC2, + publicIpSource: IpamPoolPublicIpSource.AMAZON, + locale: 'us-west-1', // Incorrect Region + }; + expect(() => ipamRegion.publicScope.addPool('TestPool', poolOptions)).toThrow("The provided locale 'us-west-1' is not in the operating regions."); + }); + + test('IPAM handles operating regions correctly', () => { + const new_app = new cdk.App(); + const testStack = new cdk.Stack(new_app, 'TestStack', { + env: { + region: 'us-west-1', + }, + }); + new Ipam(testStack, 'TestIpamNew', {}); + Template.fromStack(testStack).hasResourceProperties( + 'AWS::EC2::IPAM', { + OperatingRegions: [ + { + RegionName: 'us-west-1', + }, + ], + }, + ); + }); });// End Test diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/route.test.ts b/packages/@aws-cdk/aws-ec2-alpha/test/route.test.ts index 0428e3458e352..fb4829af8ad78 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/route.test.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/route.test.ts @@ -3,7 +3,7 @@ import * as vpc from '../lib/vpc-v2'; import * as subnet from '../lib/subnet-v2'; import { CfnEIP, GatewayVpcEndpoint, GatewayVpcEndpointAwsService, SubnetType, VpnConnectionType } from 'aws-cdk-lib/aws-ec2'; import * as route from '../lib/route'; -import { Template } from 'aws-cdk-lib/assertions'; +import { Match, Template } from 'aws-cdk-lib/assertions'; describe('EC2 Routing', () => { let stack: cdk.Stack; @@ -405,6 +405,32 @@ describe('EC2 Routing', () => { }); }); + test('NatGW throws error if both VPC and allocationID is not provided', () => { + expect(() => new route.NatGateway(stack, 'TestNATGW', { + subnet: mySubnet, + connectivityType: route.NatConnectivityType.PUBLIC, + maxDrainDuration: cdk.Duration.seconds(2001), + })).toThrow('Either provide vpc or allocationId'); + }); + + test('NatGW does not create EIP or use allocationId in case of private NAT gateway', () => { + // WHEN + new route.NatGateway(stack, 'NGW', { + vpc: myVpc, + subnet: mySubnet, + connectivityType: route.NatConnectivityType.PRIVATE, + }); + + // THEN + Template.fromStack(stack).resourceCountIs('AWS::EC2::EIP', 0); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::NatGateway', { + ConnectivityType: 'private', + }); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::NatGateway', Match.not({ + AllocationId: Match.anyValue(), + })); + }); + test('Route to DynamoDB Endpoint', () => { const dynamodb = new GatewayVpcEndpoint(stack, 'TestDB', { vpc: myVpc, @@ -512,6 +538,40 @@ describe('EC2 Routing', () => { }, }); }); + + test('Route throws error if no target is specified', () => { + expect(() => { + routeTable.addRoute('testRoute', '0.0.0.0', {}); + }).toThrow('Exactly one of `gateway` or `endpoint` must be specified.'); + }); + + test('Route throws error if both endpoint and gateway is specified as target', () => { + const eigw = new route.EgressOnlyInternetGateway(stack, 'TestEIGW', { + vpc: myVpc, + }); + + const dynamodb = new GatewayVpcEndpoint(stack, 'TestDB', { + vpc: myVpc, + service: GatewayVpcEndpointAwsService.DYNAMODB, + }); + expect(() => { + routeTable.addRoute('testRoute', '::/0', { + gateway: eigw, + endpoint: dynamodb, + }); + }).toThrow('Exactly one of `gateway` or `endpoint` must be specified.'); + }); + + test('EIGW throws error in case destination is set to an IPv4 address', () => { + const eigw = new route.EgressOnlyInternetGateway(stack, 'TestEIGW', { + vpc: myVpc, + }); + expect(() => { + routeTable.addRoute('testRoute', '0.0.0.0', { + gateway: eigw, + }); + }).toThrow('Egress only internet gateway does not support IPv4 routing'); + }); }); describe('VPCPeeringConnection', () => { @@ -523,6 +583,7 @@ describe('VPCPeeringConnection', () => { let vpcA: vpc.VpcV2; let vpcB: vpc.VpcV2; let vpcC: vpc.VpcV2; + let vpcD: vpc.VpcV2; beforeEach(() => { const app = new cdk.App({ @@ -545,7 +606,29 @@ describe('VPCPeeringConnection', () => { vpcC = new vpc.VpcV2(stackC, 'VpcC', { primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), }); + // Same Account VPC + vpcD = new vpc.VpcV2(stackC, 'VpcD', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.3.0.0/16'), + }); + }); + + test('Creates a same account VPC peering connection', () => { + // Create VPC peering connection between vpcC and vpcD in same account + new route.VPCPeeringConnection(stackC, 'TestPeeringConnection', { + requestorVpc: vpcC, + acceptorVpc: vpcD, + }); + const template = Template.fromStack(stackC); + template.hasResourceProperties('AWS::EC2::VPCPeeringConnection', { + VpcId: { + 'Fn::GetAtt': ['VpcC211819BA', 'VpcId'], + }, + PeerVpcId: { + 'Fn::GetAtt': ['VpcD66D5BFD0', 'VpcId'], + }, + PeerRegion: 'us-west-2', + }); }); test('Creates a cross account VPC peering connection', () => { @@ -628,15 +711,35 @@ describe('VPCPeeringConnection', () => { }); test('CIDR block overlap with primary CIDR block should throw error', () => { - const vpcD = new vpc.VpcV2(stackA, 'VpcD', { + const testVpc = new vpc.VpcV2(stackA, 'TestVpc', { primaryAddressBlock: vpc.IpAddresses.ipv4('10.0.0.0/16'), }); - expect(() => { new route.VPCPeeringConnection(stackA, 'TestPeering', { requestorVpc: vpcA, - acceptorVpc: vpcD, + acceptorVpc: testVpc, }); }).toThrow(/CIDR block should not overlap with each other for establishing a peering connection/); }); + + test('Can create route for VPC peering connection as a target', () => { + const peering = new route.VPCPeeringConnection(stackC, 'TestPeering', { + requestorVpc: vpcC, + acceptorVpc: vpcD, + }); + + const routeTable = new route.RouteTable(stackC, 'TestRouteTable', { + vpc: vpcD, + }); + + routeTable.addRoute('TestRoute', '172.16.0.0/16', { + gateway: peering, + }); + const template = Template.fromStack(stackC); + template.hasResourceProperties('AWS::EC2::Route', { + VpcPeeringConnectionId: { 'Fn::GetAtt': ['TestPeeringVPCPeeringConnection0E2D1596', 'Id'] }, + RouteTableId: { 'Fn::GetAtt': ['TestRouteTableC34C2E1C', 'RouteTableId'] }, + DestinationCidrBlock: '172.16.0.0/16', + }); + }); }); diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/util.ts b/packages/@aws-cdk/aws-ec2-alpha/test/subnet-test-util.ts similarity index 85% rename from packages/@aws-cdk/aws-ec2-alpha/test/util.ts rename to packages/@aws-cdk/aws-ec2-alpha/test/subnet-test-util.ts index 3af7b6db70bd1..4e2e5f1a8c760 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/util.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/subnet-test-util.ts @@ -13,9 +13,10 @@ export function createTestSubnet( subnetType: SubnetType; addressFamily?: AddressFamily; ipv6Cidr?: subnet.IpCidr; + assignIpv6AddressOnCreation?: boolean; }, ): subnet.SubnetV2 { - const { vpcV2, availabilityZone, cidrBlock, subnetType, ipv6Cidr } = config; + const { vpcV2, availabilityZone, cidrBlock, subnetType, ipv6Cidr, assignIpv6AddressOnCreation } = config; return new subnet.SubnetV2(stack, 'TestSubnet', { vpc: vpcV2, @@ -23,5 +24,6 @@ export function createTestSubnet( ipv4CidrBlock: cidrBlock, subnetType, ipv6CidrBlock: ipv6Cidr, + assignIpv6AddressOnCreation, }); } diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/subnet-v2.test.ts b/packages/@aws-cdk/aws-ec2-alpha/test/subnet-v2.test.ts index a78c1c3806a1a..85819c87fee60 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/subnet-v2.test.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/subnet-v2.test.ts @@ -4,7 +4,7 @@ import * as vpc from '../lib/vpc-v2'; import * as subnet from '../lib/subnet-v2'; import { NetworkAcl, SubnetType } from 'aws-cdk-lib/aws-ec2'; import { AddressFamily, AwsServiceName, Ipam, IpamPoolPublicIpSource } from '../lib/ipam'; -import { createTestSubnet } from './util'; +import { createTestSubnet } from './subnet-test-util'; /** * Test suite for the SubnetV2 class. @@ -322,4 +322,85 @@ describe('Subnet V2 with custom IP and routing', () => { expect(Template.fromStack(stack).hasResource('AWS::EC2::SubnetNetworkAclAssociation', {})); }); + + test('Subnet Creation throws error if assignIPv6 set to true with no IPv6 CIDR', () => { + const testVpc = new vpc.VpcV2(stack, 'TestVPC', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.amazonProvidedIpv6( + { cidrBlockName: 'SecondaryAddress' })], + }); + + const subnetConfig = { + vpcV2: testVpc, + availabilityZone: 'us-east-1a', + cidrBlock: new subnet.IpCidr('10.1.0.0/24'), + subnetType: SubnetType.PUBLIC, + assignIpv6AddressOnCreation: true, + }; + expect(() => createTestSubnet(stack, subnetConfig)).toThrow('IPv6 CIDR block is required when assigning IPv6 address on creation'); + }); + + test('Subnet field assignIpv6AddressOnCreation is set to true with IPv6 CIDR as an input', () => { + const testVpc = new vpc.VpcV2(stack, 'TestVPC', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.amazonProvidedIpv6( + { cidrBlockName: 'SecondaryAddress' })], + }); + + const subnetConfig = { + vpcV2: testVpc, + availabilityZone: 'us-east-1a', + cidrBlock: new subnet.IpCidr('10.1.0.0/24'), + ipv6Cidr: new subnet.IpCidr('2001:db8:1::/64'), + subnetType: SubnetType.PUBLIC, + assignIpv6AddressOnCreation: true, + }; + createTestSubnet(stack, subnetConfig); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { + AssignIpv6AddressOnCreation: true, + Ipv6CidrBlock: '2001:db8:1::/64', + }); + }); + + test('Subnet field assignIpv6AddressOnCreation is set to false by default with IPv6 CIDR as an input', () => { + const testVpc = new vpc.VpcV2(stack, 'TestVPC', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.amazonProvidedIpv6( + { cidrBlockName: 'SecondaryAddress' })], + }); + + const subnetConfig = { + vpcV2: testVpc, + availabilityZone: 'us-east-1a', + cidrBlock: new subnet.IpCidr('10.1.0.0/24'), + ipv6Cidr: new subnet.IpCidr('2001:db8:1::/64'), + subnetType: SubnetType.PUBLIC, + }; + createTestSubnet(stack, subnetConfig); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { + AssignIpv6AddressOnCreation: false, + Ipv6CidrBlock: '2001:db8:1::/64', + }); + }); + + test('Subnet creates custom route Table if not provided as an input', () => { + const testVpc = new vpc.VpcV2(stack, 'TestVPC', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + }); + // Subnet with no route Table as an input + const subnetConfig = { + vpcV2: testVpc, + availabilityZone: 'us-east-1a', + cidrBlock: new subnet.IpCidr('10.1.0.0/24'), + subnetType: SubnetType.PUBLIC, + }; + createTestSubnet(stack, subnetConfig); + Template.fromStack(stack).hasResource('AWS::EC2::RouteTable', 1); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SubnetRouteTableAssociation', { + SubnetId: { + Ref: 'TestSubnet2A4BE4CA', + }, + RouteTableId: { 'Fn::GetAtt': ['TestSubnetRouteTable5AF4379E', 'RouteTableId'] }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/util.test.ts b/packages/@aws-cdk/aws-ec2-alpha/test/util.test.ts index 56764458f3044..24ccc90ece598 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/util.test.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/util.test.ts @@ -1,4 +1,4 @@ -import { CidrBlock, CidrBlockIpv6 } from '../lib/util'; +import { CidrBlock, CidrBlockIpv6, NetworkUtils } from '../lib/util'; describe('Tests for the CidrBlock.rangesOverlap method to check if IPv4 ranges overlap', () =>{ test('Should return false for non-overlapping IP ranges', () => { @@ -45,4 +45,29 @@ describe('Tests for the CidrBlock.rangesOverlap method to check if IPv4 ranges o const testCidr = new CidrBlockIpv6('2001:db8::/32'); expect(testCidr.rangesOverlap('2001:db8::1/64', '2001:db8::1/60')).toBe(true); }); + + test('valid IPv4 addresses return true', () => { + const validIps = [ + '192.168.1.1', + '10.0.0.0', + '172.16.254.1', + '0.0.0.0', + '255.255.255.255', + ]; + + validIps.forEach(ip => { + expect(NetworkUtils.validIp(ip)).toBe(true); + }); + }); + + test('invalid IP addresses return false', () => { + const invalidIps = [ + '256.1.2.3', // octet > 255 + '1.2.3.256', // octet > 255 + '1.2.3.4.5', + ]; + invalidIps.forEach(ip => { + expect(NetworkUtils.validIp(ip)).toBe(false); + }); + }); }); diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/vpc-add-method.test.ts b/packages/@aws-cdk/aws-ec2-alpha/test/vpc-add-method.test.ts index 2e6f64caa20d5..f58c75fc260e8 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/vpc-add-method.test.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/vpc-add-method.test.ts @@ -9,6 +9,9 @@ describe('Vpc V2 with full control', () => { let stack: cdk.Stack; let myVpc: vpc.VpcV2; let mySubnet: SubnetV2; + let routeTable1: route.RouteTable; + let testSubnet1: SubnetV2; + let testSubnet2: SubnetV2; beforeEach(() => { const app = new cdk.App({ @@ -25,10 +28,30 @@ describe('Vpc V2 with full control', () => { }); mySubnet = new SubnetV2(stack, 'TestSubnet', { vpc: myVpc, - ipv4CidrBlock: new IpCidr('10.1.0.0/24'), + ipv4CidrBlock: new IpCidr('10.1.1.0/24'), availabilityZone: 'ap-south-1b', subnetType: SubnetType.PUBLIC, - ipv6CidrBlock: new IpCidr('2001:db8::/48'), + ipv6CidrBlock: new IpCidr('2001:db8:0::/64'), + }); + routeTable1 = new route.RouteTable(stack, 'PrivateSubnetRouteTable1', { + vpc: myVpc, + }); + testSubnet1 = new SubnetV2(stack, 'TestSubnet1', { + vpc: myVpc, + ipv4CidrBlock: new IpCidr('10.1.2.0/24'), + ipv6CidrBlock: new IpCidr('2001:db8:1::/64'), + routeTable: routeTable1, + availabilityZone: 'ap-south-1a', + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + }); + + testSubnet2 = new SubnetV2(stack, 'TestSubnet2', { + vpc: myVpc, + ipv4CidrBlock: new IpCidr('10.1.3.0/24'), + ipv6CidrBlock: new IpCidr('2001:db8:2::/64'), + routeTable: routeTable1, + availabilityZone: 'ap-south-1b', + subnetType: SubnetType.PRIVATE_WITH_EGRESS, }); }); test('Method to add a new Egress-Only IGW', () => { @@ -305,6 +328,86 @@ describe('Vpc V2 with full control', () => { }); }); + test('addInternetGateway throws error if subnet type provided is not available', () => { + expect(() => { + myVpc.addInternetGateway({ + subnets: [{ subnetType: SubnetType.PRIVATE_ISOLATED }], + }); + }).toThrow("There are no 'Isolated' subnet groups in this VPC. Available types: Deprecated_Private_NAT,Private,Deprecated_Private,Public"); + }); + + test('addInternetGateway adds route for all selected subnets if given as an input', () => { + myVpc.addInternetGateway({ + subnets: [{ subnetType: SubnetType.PRIVATE_WITH_EGRESS }], + }); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Route', { + GatewayId: { + 'Fn::GetAtt': ['TestVpcInternetGatewayIGW4C825874', 'InternetGatewayId'], + }, + RouteTableId: { // Private Subnet RouteTable + 'Fn::GetAtt': ['PrivateSubnetRouteTable1RouteTable8DA475CB', 'RouteTableId'], + }, + }); + // Should be created only for Private Egress Subnet and not for Public Subnet + Template.fromStack(stack).hasResource('AWS::EC2::Route', 1); + }); + + test('addInternetGateway adds route for multiple subnets when filtered by subnet IDs', () => { + // Add Internet Gateway with multiple subnet IDs + myVpc.addInternetGateway({ + subnets: [testSubnet1, testSubnet2], + }); + + // Verify routes are created for both subnets + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Route', { + GatewayId: { + 'Fn::GetAtt': ['TestVpcInternetGatewayIGW4C825874', 'InternetGatewayId'], + }, + RouteTableId: { + 'Fn::GetAtt': ['PrivateSubnetRouteTable1RouteTable8DA475CB', 'RouteTableId'], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Route', { + GatewayId: { + 'Fn::GetAtt': ['TestVpcInternetGatewayIGW4C825874', 'InternetGatewayId'], + }, + RouteTableId: { + 'Fn::GetAtt': ['PrivateSubnetRouteTable1RouteTable8DA475CB', 'RouteTableId'], + }, + }); + // Verify two routes are created + Template.fromStack(stack).hasResource('AWS::EC2::Route', 2); + }); + + test('addEgressInternetGateway adds route for multiple subnets when filtered by subnet IDs', () => { + // Add Internet Gateway with multiple subnet IDs + myVpc.addEgressOnlyInternetGateway({ + subnets: [testSubnet1, testSubnet2], + }); + + // Verify routes are created for both subnets + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Route', { + EgressOnlyInternetGatewayId: { + 'Fn::GetAtt': ['TestVpcEgressOnlyGWEIGW5A79987F', 'Id'], + }, + RouteTableId: { + 'Fn::GetAtt': ['PrivateSubnetRouteTable1RouteTable8DA475CB', 'RouteTableId'], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Route', { + EgressOnlyInternetGatewayId: { + 'Fn::GetAtt': ['TestVpcEgressOnlyGWEIGW5A79987F', 'Id'], + }, + RouteTableId: { + 'Fn::GetAtt': ['PrivateSubnetRouteTable1RouteTable8DA475CB', 'RouteTableId'], + }, + }); + // Verify two routes are created + Template.fromStack(stack).hasResource('AWS::EC2::Route', 2); + }); + test('Throws error if there is already an IGW attached', () => { myVpc.addInternetGateway(); expect(() => { @@ -388,7 +491,7 @@ describe('Vpc V2 with full control', () => { type: VpnConnectionType.IPSEC_1, vpnRoutePropagation: [{ subnetType: SubnetType.PRIVATE_ISOLATED }], }); - }).toThrow("There are no 'Isolated' subnet groups in this VPC. Available types: Public"); + }).toThrow("There are no 'Isolated' subnet groups in this VPC. Available types: Deprecated_Private_NAT,Private,Deprecated_Private,Public"); }); test('Throws error when VPN GW is already enabled', () => { diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/vpc-v2.test.ts b/packages/@aws-cdk/aws-ec2-alpha/test/vpc-v2.test.ts index beb8fcda41c79..efb323a3f8771 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/vpc-v2.test.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/vpc-v2.test.ts @@ -305,4 +305,66 @@ describe('Vpc V2 with full control', () => { const template = Template.fromStack(stack); template.resourceCountIs('AWS::EC2::VPCCidrBlock', 2); }); + + test('Set field `useIpv6` to true if secondary address is defined using IPAM IPv6', () => { + const ipv6Vpc = new vpc.VpcV2(stack, 'TestVpc', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.ipv6Ipam({ + ipamPool: new Ipam(stack, 'TestIpam', { + operatingRegion: ['us-west-1'], + }).publicScope.addPool('PublicPool0', { + addressFamily: AddressFamily.IP_V6, + awsService: AwsServiceName.EC2, + publicIpSource: IpamPoolPublicIpSource.AMAZON, + locale: 'us-west-1', + }), + netmaskLength: 64, + cidrBlockName: 'IPv6Ipam', + })], + }, + ); + expect(ipv6Vpc.useIpv6).toBe(true); + }); + + test('Set field `useIpv6` to true if secondary address is defined using Amazon Provided IPv6', () => { + const ipv6Vpc = new vpc.VpcV2(stack, 'TestVpc', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.amazonProvidedIpv6({ + cidrBlockName: 'Ipv6Amazon', + })], + }, + ); + expect(ipv6Vpc.useIpv6).toBe(true); + }); + + test('Set field `useIpv6` to true if secondary address is defined using BYOIP IPv6', () => { + const ipv6Vpc = new vpc.VpcV2(stack, 'TestVpc', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.ipv6ByoipPool({ + ipv6PoolId: 'test-Byoip-pool', + ipv6CidrBlock: '2001:db8::/32', + cidrBlockName: 'BYOIP', + })], + }, + ); + expect(ipv6Vpc.useIpv6).toBe(true); + }); + + test('Set field `useIpv6` to false if no IPv6 address is attached to VPC', () => { + const testVpc = new vpc.VpcV2(stack, 'TestVpc', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.ipv4('10.2.0.0/16', { + cidrBlockName: 'SecondaryAddress', + })], + }, + ); + expect(testVpc.useIpv6).toBe(false); + }); + + test('VPC throws error is secondary CIDR block name is not provided', () => { + expect(() => new vpc.VpcV2(stack, 'TestVPC', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.ipv4('10.2.0.0/16')], + })).toThrow('Cidr Block Name is required to create secondary IP address'); + }); }); diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/vpcv2-import.test.ts b/packages/@aws-cdk/aws-ec2-alpha/test/vpcv2-import.test.ts index 212eb12a47af5..89943e674b1c1 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/vpcv2-import.test.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/vpcv2-import.test.ts @@ -214,4 +214,100 @@ describe('Vpc V2 with full control', () => { SubnetId: 'mockSubnetId', }); }); + + test('Import method fromVpcV2Attributes correctly categorizes subnets by type', () => { + const vpc = VpcV2.fromVpcV2Attributes(stack, 'ImportedVPC', { + vpcId: 'vpc-123456', + vpcCidrBlock: '10.0.0.0/16', + subnets: [ + { + subnetId: 'subnet-private1', + availabilityZone: 'us-east-1a', + ipv4CidrBlock: '10.0.1.0/24', + routeTableId: 'rt-private1', + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + subnetName: 'Private1', + }, + { + subnetId: 'subnet-private2', + availabilityZone: 'us-east-1b', + ipv4CidrBlock: '10.0.2.0/24', + routeTableId: 'rt-private2', + subnetType: SubnetType.PRIVATE_WITH_NAT, + subnetName: 'Private2', + }, + { + subnetId: 'subnet-public1', + availabilityZone: 'us-east-1a', + ipv4CidrBlock: '10.0.4.0/24', + routeTableId: 'rt-public1', + subnetType: SubnetType.PUBLIC, + subnetName: 'Public1', + }, + { + subnetId: 'subnet-isolated1', + availabilityZone: 'us-east-1b', + ipv4CidrBlock: '10.0.5.0/24', + routeTableId: 'rt-isolated1', + subnetType: SubnetType.PRIVATE_ISOLATED, + subnetName: 'Isolated1', + }, + ], + }); + + // Verify private subnets + expect(vpc.privateSubnets.length).toBe(2); + + // Verify public subnets + expect(vpc.publicSubnets.length).toBe(1); + + // Verify isolated subnets + expect(vpc.isolatedSubnets.length).toBe(1); + }); + + test('Import method fromVpcV2Attributes use default names for subnets if not set under field `subnetName` while importing', () => { + const vpc = VpcV2.fromVpcV2Attributes(stack, 'ImportedVPC', { + vpcId: 'vpc-123456', + vpcCidrBlock: '10.0.0.0/16', + subnets: [ + { + subnetId: 'subnet-private1', + availabilityZone: 'us-east-1a', + ipv4CidrBlock: '10.0.1.0/24', + routeTableId: 'rt-private1', + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + }, + { + subnetId: 'subnet-public1', + availabilityZone: 'us-east-1a', + ipv4CidrBlock: '10.0.2.0/24', + routeTableId: 'rt-public1', + subnetType: SubnetType.PUBLIC, + }, + { + subnetId: 'subnet-isolated1', + availabilityZone: 'us-east-1a', + ipv4CidrBlock: '10.0.3.0/24', + routeTableId: 'rt-isolated1', + subnetType: SubnetType.PRIVATE_ISOLATED, + }, + ], + }); + + // Verify default names are used + expect(vpc.privateSubnets[0].node.id).toBe('ImportedPrivateSubnet'); + expect(vpc.publicSubnets[0].node.id).toBe('ImportedPublicSubnet'); + expect(vpc.isolatedSubnets[0].node.id).toBe('ImportedIsolatedSubnet'); + }); + + test('handles undefined subnets', () => { + const vpc = VpcV2.fromVpcV2Attributes(stack, 'ImportedVPC', { + vpcId: 'vpc-123456', + vpcCidrBlock: '10.0.0.0/16', + }); + + expect(vpc.privateSubnets).toHaveLength(0); + expect(vpc.publicSubnets).toHaveLength(0); + expect(vpc.isolatedSubnets).toHaveLength(0); + }); });