Skip to content

Commit

Permalink
chore(VpcV2): increasing test coverage for graduation
Browse files Browse the repository at this point in the history
  • Loading branch information
shikha372 committed Jan 24, 2025
1 parent d6e3c61 commit bf65780
Show file tree
Hide file tree
Showing 13 changed files with 612 additions and 32 deletions.
29 changes: 29 additions & 0 deletions packages/@aws-cdk/aws-ec2-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,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:

Expand All @@ -546,6 +547,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.
Expand Down
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.
Expand Down Expand Up @@ -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');
}

Expand Down
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
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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)) {
Expand Down
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
Expand Up @@ -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[];
}

/**
Expand Down Expand Up @@ -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);
}
});
}
}
Expand Down Expand Up @@ -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));
}
}

Expand All @@ -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`, {
Expand Down
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
Expand Up @@ -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
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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({
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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',
});
});
});
Loading

0 comments on commit bf65780

Please sign in to comment.