Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(VpcV2): increasing test coverage for graduation
Browse files Browse the repository at this point in the history
shikha372 committed Jan 24, 2025

Verified

This commit was signed with the committer’s verified signature.
JMazurkiewicz Jakub Mazurkiewicz
1 parent d6e3c61 commit d0d5d22
Showing 13 changed files with 618 additions and 32 deletions.
35 changes: 35 additions & 0 deletions packages/@aws-cdk/aws-ec2-alpha/README.md
Original file line number Diff line number Diff line change
@@ -66,6 +66,12 @@ 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 <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 +533,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 +553,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.
6 changes: 0 additions & 6 deletions packages/@aws-cdk/aws-ec2-alpha/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config');
module.exports = {
...baseConfig,
coverageThreshold: {
global: {
statements: 75,
branches: 63,
},
},
};;
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-ec2-alpha/lib/ipam.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CfnIPAM, CfnIPAMPool, CfnIPAMPoolCidr, CfnIPAMScope } from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
import { Lazy, Names, Resource, Stack, Tags } from 'aws-cdk-lib';
import { Lazy, Names, Resource, Stack, Tags, Token } from 'aws-cdk-lib';

/**
* Represents the address family for IP addresses in an IPAM pool.
@@ -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 && Token.isUnresolved(Stack.of(this).region)) {
throw new Error('Please provide at least one operating region');
}

12 changes: 6 additions & 6 deletions packages/@aws-cdk/aws-ec2-alpha/lib/route.ts
Original file line number Diff line number Diff line change
@@ -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)) {
34 changes: 26 additions & 8 deletions packages/@aws-cdk/aws-ec2-alpha/lib/vpc-v2-base.ts
Original file line number Diff line number Diff line change
@@ -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<string>();
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,19 @@ 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<string>();
const subnets = flatten(options.subnets.map(s => this.selectSubnets(s).subnets));
subnets.forEach((subnet) => {
if (!processedSubnets.has(subnet.node.id)) {
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 +511,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`, {
67 changes: 67 additions & 0 deletions packages/@aws-cdk/aws-ec2-alpha/test/ipam.test.ts
Original file line number Diff line number Diff line change
@@ -155,4 +155,71 @@ 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 both operating region and stack region is not configured', () => {
const app = new cdk.App();
const stack_new = new cdk.Stack(app, 'TestStack', {});
expect(() => new Ipam(stack_new, 'TestIpam')).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 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.");
});
});// End Test
111 changes: 107 additions & 4 deletions packages/@aws-cdk/aws-ec2-alpha/test/route.test.ts
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
Original file line number Diff line number Diff line change
@@ -13,15 +13,17 @@ 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,
availabilityZone,
ipv4CidrBlock: cidrBlock,
subnetType,
ipv6CidrBlock: ipv6Cidr,
assignIpv6AddressOnCreation,
});
}
83 changes: 82 additions & 1 deletion packages/@aws-cdk/aws-ec2-alpha/test/subnet-v2.test.ts
Original file line number Diff line number Diff line change
@@ -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'] },
});
});
});
27 changes: 26 additions & 1 deletion packages/@aws-cdk/aws-ec2-alpha/test/util.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
109 changes: 106 additions & 3 deletions packages/@aws-cdk/aws-ec2-alpha/test/vpc-add-method.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
62 changes: 62 additions & 0 deletions packages/@aws-cdk/aws-ec2-alpha/test/vpc-v2.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
96 changes: 96 additions & 0 deletions packages/@aws-cdk/aws-ec2-alpha/test/vpcv2-import.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit d0d5d22

Please sign in to comment.