Skip to content

Commit bcb7f9b

Browse files
authored
fix(ec2-alpha): readme updates, new unit tests, logic update (#33086)
### Issue # (if applicable) Closes #30762 . ### Reason for this change Adding more unit tests to meet the global coverage before module graduation to `developer-preview`. ### Description of changes - Add more unit test case to cover all `if branches` test cases. - As per the discussion with service team, added optional field under IGW to allow users to choose the subnets for gateway routing, as there can be Public Subnet without an IGW attached( eg. using VPNGW to access internet). - Update IPAM README to higlight the problem of IPAM pool deletion as discussed with service team. - Update SubnetV2 README to higlight that a custom route table is being created through CDK. ### Describe any new or updated permissions being added No changes to IAM permissions. ### Description of how you validated changes `yarn build` `yarn test` ``` yarn run v1.22.21 $ cdk-test PASS test/ipam.test.ts PASS test/subnet-v2.test.ts PASS test/vpc-tagging.test.ts PASS test/util.test.ts PASS test/route.test.ts PASS test/vpc-add-method.test.ts PASS test/vpcv2-import.test.ts PASS test/vpc-v2.test.ts =============================== Coverage summary =============================== Statements : 89.88% ( 640/712 ) Branches : 81.68% ( 223/273 ) Functions : 82.6% ( 133/161 ) Lines : 89.89% ( 614/683 ) ================================================================================ Test Suites: 8 passed, 8 total Tests: 126 passed, 126 total Snapshots: 0 total Time: 2.244 s Ran all test suites. Verifying integration test snapshots... UNCHANGED integ.byoip-ipv6 0.888s UNCHANGED integ.ipam 0.928s UNCHANGED integ.subnet-v2 1.036s UNCHANGED integ.vpc-v2-alpha 1.047s UNCHANGED integ.test-import 1.053s UNCHANGED integ.peering-cross-account 1.101s UNCHANGED integ.vpc-v2-tagging 1.264s UNCHANGED integ.route-v2 1.29s Snapshot Results: Tests: 8 passed, 8 total Tests successful. Total time (4.5s) | /Users/shikagg/vpc_peering/aws-cdk/node_modules/jest/bin/jest.js (2.7s) | integ-runner (1.8s) ✨ Done in 4.87s. ``` ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) BREAKING CHANGE: `operatingRegion` property under IPAM class is now renamed to `operatingRegions`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 1b666db commit bcb7f9b

16 files changed

+675
-44
lines changed

packages/@aws-cdk/aws-ec2-alpha/README.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ new VpcV2(this, 'Vpc', {
4141

4242
`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.
4343
This new construct can be used to add subnets to a `VpcV2` instance:
44+
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.
4445

4546
```ts
4647

@@ -66,11 +67,19 @@ By default `VpcV2` uses `10.0.0.0/16` as the primary CIDR if none is defined.
6667
Additional CIDRs can be adding to the VPC via the `secondaryAddressBlocks` prop.
6768
The following example illustrates the different options of defining the address blocks:
6869

70+
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
71+
72+
```shell
73+
aws ec2 get-ipam-pool-allocations --ipam-pool-id <ipam-pool-id>
74+
```
75+
76+
Ref: https://docs.aws.amazon.com/cli/latest/reference/ec2/get-ipam-pool-allocations.html
77+
6978
```ts
7079

7180
const stack = new Stack();
7281
const ipam = new Ipam(this, 'Ipam', {
73-
operatingRegion: ['us-west-1']
82+
operatingRegions: ['us-west-1']
7483
});
7584
const ipamPublicPool = ipam.publicScope.addPool('PublicPoolA', {
7685
addressFamily: AddressFamily.IP_V6,
@@ -527,6 +536,7 @@ For more information, see [Enable VPC internet access using internet gateways](h
527536

528537
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.
529538
Instead of using the default settings, you can configure a custom destination range by providing an optional input `destination` to the method.
539+
In addition to the custom IP range, you can also choose to filter subnets where default routes should be created.
530540

531541
The code example below shows how to add an internet gateway with a custom outbound destination IP range:
532542

@@ -546,6 +556,34 @@ myVpc.addInternetGateway({
546556
});
547557
```
548558

559+
The following code examples demonstrates how to add an internet gateway with a custom outbound destination IP range for specific subnets:
560+
561+
```ts
562+
const stack = new Stack();
563+
const myVpc = new VpcV2(this, 'Vpc');
564+
565+
const mySubnet = new SubnetV2(this, 'Subnet', {
566+
vpc: myVpc,
567+
availabilityZone: 'eu-west-2a',
568+
ipv4CidrBlock: new IpCidr('10.0.0.0/24'),
569+
subnetType: SubnetType.PUBLIC });
570+
571+
myVpc.addInternetGateway({
572+
ipv4Destination: '192.168.0.0/16',
573+
subnets: [mySubnet],
574+
});
575+
```
576+
577+
```ts
578+
const stack = new Stack();
579+
const myVpc = new VpcV2(this, 'Vpc');
580+
581+
myVpc.addInternetGateway({
582+
ipv4Destination: '192.168.0.0/16',
583+
subnets: [{subnetType: SubnetType.PRIVATE_WITH_EGRESS}],
584+
});
585+
```
586+
549587
## Importing an existing VPC
550588

551589
You can import an existing VPC and its subnets using the `VpcV2.fromVpcV2Attributes()` method or an individual subnet using `SubnetV2.fromSubnetV2Attributes()` method.
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
11
const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config');
22
module.exports = {
33
...baseConfig,
4-
coverageThreshold: {
5-
global: {
6-
statements: 75,
7-
branches: 63,
8-
},
9-
},
104
};;

packages/@aws-cdk/aws-ec2-alpha/lib/ipam.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export interface IpamProps {
6262
*
6363
* @default - Stack.region if defined in the stack
6464
*/
65-
readonly operatingRegion?: string[];
65+
readonly operatingRegions?: string[];
6666

6767
/**
6868
* Name of IPAM that can be used for tagging resource
@@ -511,11 +511,11 @@ export class Ipam extends Resource {
511511
if (props?.ipamName) {
512512
Tags.of(this).add(NAME_TAG, props.ipamName);
513513
}
514-
if (!props?.operatingRegion && !Stack.of(this).region) {
514+
if (props?.operatingRegions && (props.operatingRegions.length === 0)) {
515515
throw new Error('Please provide at least one operating region');
516516
}
517517

518-
this.operatingRegions = props?.operatingRegion ?? [Stack.of(this).region];
518+
this.operatingRegions = props?.operatingRegions ?? [Stack.of(this).region];
519519
this.ipamName = props?.ipamName;
520520

521521
this._ipam = new CfnIPAM(this, 'Ipam', {

packages/@aws-cdk/aws-ec2-alpha/lib/route.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -605,12 +605,8 @@ export class RouteTargetType {
605605
readonly endpoint?: IVpcEndpoint;
606606

607607
constructor(props: RouteTargetProps) {
608-
if ((props.gateway && props.endpoint) || (!props.gateway && !props.endpoint)) {
609-
throw new Error('Exactly one of `gateway` or `endpoint` must be specified.');
610-
} else {
611-
this.gateway = props.gateway;
612-
this.endpoint = props.endpoint;
613-
}
608+
this.gateway = props.gateway;
609+
this.endpoint = props.endpoint;
614610
}
615611
}
616612

@@ -729,6 +725,10 @@ export class Route extends Resource implements IRouteV2 {
729725
if (this.target.gateway?.routerType === RouterType.EGRESS_ONLY_INTERNET_GATEWAY && isDestinationIpv4) {
730726
throw new Error('Egress only internet gateway does not support IPv4 routing');
731727
}
728+
729+
if ((props.target.gateway && props.target.endpoint) || (!props.target.gateway && !props.target.endpoint)) {
730+
throw new Error('Exactly one of `gateway` or `endpoint` must be specified.');
731+
}
732732
this.targetRouterType = this.target.gateway ? this.target.gateway.routerType : RouterType.VPC_ENDPOINT;
733733
// Gateway generates route automatically via its RouteTable, thus we don't need to generate the resource for it
734734
if (!(this.target.endpoint instanceof GatewayVpcEndpoint)) {

packages/@aws-cdk/aws-ec2-alpha/lib/vpc-v2-base.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ export interface InternetGatewayOptions{
6060
* @default - provisioned without a resource name
6161
*/
6262
readonly internetGatewayName?: string;
63+
64+
/**
65+
* List of subnets where route to IGW will be added
66+
*
67+
* @default - route created for all subnets with Type `SubnetType.Public`
68+
*/
69+
readonly subnets?: SubnetSelection[];
6370
}
6471

6572
/**
@@ -437,9 +444,14 @@ export abstract class VpcV2Base extends Resource implements IVpcV2 {
437444
}
438445

439446
if (options?.subnets) {
447+
// Use Set to ensure unique subnets
448+
const processedSubnets = new Set<string>();
440449
const subnets = flatten(options.subnets.map(s => this.selectSubnets(s).subnets));
441450
subnets.forEach((subnet) => {
442-
this.createEgressRoute(subnet, egw, options.destination);
451+
if (!processedSubnets.has(subnet.node.id)) {
452+
this.createEgressRoute(subnet, egw, options.destination);
453+
processedSubnets.add(subnet.node.id);
454+
}
443455
});
444456
}
445457
}
@@ -476,9 +488,23 @@ export abstract class VpcV2Base extends Resource implements IVpcV2 {
476488
this._internetConnectivityEstablished.add(igw);
477489
this._internetGatewayId = igw.routerTargetId;
478490

479-
// If there are no public subnets defined, no default route will be added
480-
if (this.publicSubnets) {
481-
this.publicSubnets.forEach( (s) => this.addDefaultInternetRoute(s, igw, options));
491+
// Add routes for subnets defined as an input
492+
if (options?.subnets) {
493+
// Use Set to ensure unique subnets
494+
const processedSubnets = new Set<string>();
495+
const subnets = flatten(options.subnets.map(s => this.selectSubnets(s).subnets));
496+
subnets.forEach((subnet) => {
497+
if (!processedSubnets.has(subnet.node.id)) {
498+
if (!this.publicSubnets.includes(subnet)) {
499+
Annotations.of(this).addWarningV2('InternetGatewayWarning',
500+
`Subnet ${subnet.node.id} is not a public subnet. Internet Gateway should be added only to public subnets.`);
501+
}
502+
this.addDefaultInternetRoute(subnet, igw, options);
503+
processedSubnets.add(subnet.node.id);
504+
}
505+
}); // If there are no input subnets defined, default route will be added to all public subnets
506+
} else if (!options?.subnets && this.publicSubnets) {
507+
this.publicSubnets.forEach((publicSubnets) => this.addDefaultInternetRoute(publicSubnets, igw, options));
482508
}
483509
}
484510

@@ -488,10 +514,6 @@ export abstract class VpcV2Base extends Resource implements IVpcV2 {
488514
*/
489515
private addDefaultInternetRoute(subnet: ISubnetV2, igw: InternetGateway, options?: InternetGatewayOptions): void {
490516

491-
if (subnet.subnetType !== SubnetType.PUBLIC) {
492-
throw new Error('No public subnets defined to add route for internet gateway');
493-
}
494-
495517
// Add default route to IGW for IPv6
496518
if (subnet.ipv6CidrBlock) {
497519
new Route(this, `${subnet.node.id}-DefaultIPv6Route`, {

packages/@aws-cdk/aws-ec2-alpha/test/integ.ipam.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const app = new cdk.App();
2323
const stack = new cdk.Stack(app, 'aws-cdk-vpcv2-alpha-integ-ipam');
2424

2525
const ipam = new Ipam(stack, 'IpamTest', {
26-
operatingRegion: ['us-west-2'],
26+
operatingRegions: ['us-west-2'],
2727
});
2828

2929
/** Test Ipam Pool Ipv4 */

packages/@aws-cdk/aws-ec2-alpha/test/integ.vpc-v2-tagging.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const natgw = vpc.addNatGateway({
5353
natgw.node.addDependency(vpnGateway);
5454

5555
const ipam = new Ipam(stack, 'IpamIntegTest', {
56-
operatingRegion: ['us-west-2'],
56+
operatingRegions: ['us-west-2'],
5757
ipamName: 'CDKIpamTestTag',
5858
});
5959

packages/@aws-cdk/aws-ec2-alpha/test/ipam.test.ts

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('IPAM Test', () => {
1818
env: envUSA,
1919
});
2020
ipam = new Ipam(stack, 'Ipam', {
21-
operatingRegion: ['us-west-2'],
21+
operatingRegions: ['us-west-2'],
2222
});
2323
});
2424

@@ -82,7 +82,7 @@ describe('IPAM Test', () => {
8282
test('Creates IPAM CIDR pool under public scope for IPv6', () => {
8383
// Create IPAM resources
8484
const ipamIpv6 = new Ipam(stack, 'TestIpam', {
85-
operatingRegion: ['us-west-2'],
85+
operatingRegions: ['us-west-2'],
8686
});
8787
const poolOptions: vpc.PoolOptions = {
8888
addressFamily: AddressFamily.IP_V6,
@@ -116,7 +116,7 @@ describe('IPAM Test', () => {
116116
test('Get region from stack env', () => {
117117
// Create IPAM resources
118118
const ipamRegion = new Ipam(stack, 'TestIpam', {
119-
operatingRegion: ['us-west-2'],
119+
operatingRegions: ['us-west-2'],
120120
});
121121
const poolOptions: vpc.PoolOptions = {
122122
addressFamily: AddressFamily.IP_V6,
@@ -155,4 +155,109 @@ describe('IPAM Test', () => {
155155
);
156156
});
157157

158+
test('IPAM throws error if awsService is not provided for IPv6 address', () => {
159+
// Create IPAM resources
160+
const ipamRegion = new Ipam(stack, 'TestIpam', {
161+
operatingRegions: ['us-west-2'],
162+
});
163+
const poolOptions: vpc.PoolOptions = {
164+
addressFamily: AddressFamily.IP_V6,
165+
publicIpSource: IpamPoolPublicIpSource.AMAZON,
166+
locale: 'us-west-2',
167+
};
168+
expect(() => ipamRegion.publicScope.addPool('TestPool', poolOptions)).toThrow('awsService is required when addressFamily is set to ipv6');
169+
});
170+
171+
test('IPAM throws error if operating region is provided as an empty array', () => {
172+
const app = new cdk.App();
173+
const stack_new = new cdk.Stack(app, 'TestStack');
174+
expect(() => new Ipam(stack_new, 'TestIpam', {
175+
operatingRegions: [],
176+
})).toThrow('Please provide at least one operating region');
177+
});
178+
179+
test('IPAM infers region from provided operating region correctly', () => {
180+
const app = new cdk.App();
181+
const stack_new = new cdk.Stack(app, 'TestStack');
182+
new Ipam(stack_new, 'TestIpam', {
183+
operatingRegions: ['us-west-2'],
184+
});
185+
Template.fromStack(stack_new).hasResourceProperties(
186+
'AWS::EC2::IPAM', {
187+
OperatingRegions: [
188+
{
189+
RegionName: 'us-west-2',
190+
},
191+
],
192+
},
193+
);
194+
});
195+
196+
test('IPAM infers region from stack if not provided under IPAM class object', () => {
197+
const app = new cdk.App();
198+
const stack_new = new cdk.Stack(app, 'TestStack', {
199+
env: {
200+
region: 'us-west-2',
201+
},
202+
});
203+
new Ipam(stack_new, 'TestIpam', {});
204+
Template.fromStack(stack_new).hasResourceProperties(
205+
'AWS::EC2::IPAM', {
206+
OperatingRegions: [
207+
{
208+
RegionName: 'us-west-2',
209+
},
210+
],
211+
},
212+
);
213+
});
214+
215+
test('IPAM refers to stack region token', () => {
216+
const app = new cdk.App();
217+
const stack_new = new cdk.Stack(app, 'TestStack');
218+
new Ipam(stack_new, 'TestIpam', {});
219+
Template.fromStack(stack_new).hasResourceProperties(
220+
'AWS::EC2::IPAM', {
221+
OperatingRegions: [
222+
{
223+
RegionName: {
224+
Ref: 'AWS::Region',
225+
},
226+
},
227+
],
228+
},
229+
);
230+
});
231+
232+
test('IPAM throws error if locale(region) of pool is not one of the operating regions', () => {
233+
const ipamRegion = new Ipam(stack, 'TestIpam', {
234+
operatingRegions: ['us-west-2'],
235+
});
236+
const poolOptions: vpc.PoolOptions = {
237+
addressFamily: AddressFamily.IP_V6,
238+
awsService: vpc.AwsServiceName.EC2,
239+
publicIpSource: IpamPoolPublicIpSource.AMAZON,
240+
locale: 'us-west-1', // Incorrect Region
241+
};
242+
expect(() => ipamRegion.publicScope.addPool('TestPool', poolOptions)).toThrow("The provided locale 'us-west-1' is not in the operating regions.");
243+
});
244+
245+
test('IPAM handles operating regions correctly', () => {
246+
const new_app = new cdk.App();
247+
const testStack = new cdk.Stack(new_app, 'TestStack', {
248+
env: {
249+
region: 'us-west-1',
250+
},
251+
});
252+
new Ipam(testStack, 'TestIpamNew', {});
253+
Template.fromStack(testStack).hasResourceProperties(
254+
'AWS::EC2::IPAM', {
255+
OperatingRegions: [
256+
{
257+
RegionName: 'us-west-1',
258+
},
259+
],
260+
},
261+
);
262+
});
158263
});// End Test

0 commit comments

Comments
 (0)