Skip to content

Commit 2392108

Browse files
authored
fix(ec2): improve errors around subnet selection (#4089)
Selecting subnets by VPC and type is a confusing topic with some API misapprehensions. Try to fix that by renaming `subnetName` to `subnetGroupName`, and giving more clear error messages about legal values in case of misuse. Closes #3859.
1 parent 37ffd09 commit 2392108

File tree

3 files changed

+105
-44
lines changed

3 files changed

+105
-44
lines changed

packages/@aws-cdk/aws-ec2/lib/util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function defaultSubnetName(type: SubnetType) {
2626
*
2727
* All subnet names look like NAME <> "Subnet" <> INDEX
2828
*/
29-
export function subnetName(subnet: ISubnet) {
29+
export function subnetGroupNameFromConstructId(subnet: ISubnet) {
3030
return subnet.node.id.replace(/Subnet\d+$/, '');
3131
}
3232

packages/@aws-cdk/aws-ec2/lib/vpc.ts

Lines changed: 71 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { CfnEIP, CfnInternetGateway, CfnNatGateway, CfnRoute, CfnVPNGateway, Cfn
55
import { CfnRouteTable, CfnSubnet, CfnSubnetRouteTableAssociation, CfnVPC, CfnVPCGatewayAttachment } from './ec2.generated';
66
import { INetworkAcl, NetworkAcl, SubnetNetworkAclAssociation } from './network-acl';
77
import { NetworkBuilder } from './network-util';
8-
import { allRouteTableIds, defaultSubnetName, ImportSubnetGroup, subnetId, subnetName } from './util';
8+
import { allRouteTableIds, defaultSubnetName, ImportSubnetGroup, subnetGroupNameFromConstructId, subnetId } from './util';
99
import { GatewayVpcEndpoint, GatewayVpcEndpointAwsService, GatewayVpcEndpointOptions } from './vpc-endpoint';
1010
import { InterfaceVpcEndpoint, InterfaceVpcEndpointOptions } from './vpc-endpoint';
1111
import { VpcLookupOptions } from './vpc-lookup';
@@ -162,22 +162,40 @@ export enum SubnetType {
162162
*/
163163
export interface SubnetSelection {
164164
/**
165-
* Place the instances in the subnets of the given type
165+
* Select all subnets of the given type
166166
*
167-
* At most one of `subnetType` and `subnetName` can be supplied.
167+
* At most one of `subnetType` and `subnetGroupName` can be supplied.
168168
*
169169
* @default SubnetType.PRIVATE
170170
*/
171171
readonly subnetType?: SubnetType;
172172

173173
/**
174-
* Place the instances in the subnets with the given name
174+
* Select the subnet group with the given name
175175
*
176-
* (This is the name supplied in subnetConfiguration).
176+
* Select the subnet group with the given name. This only needs
177+
* to be used if you have multiple subnet groups of the same type
178+
* and you need to distinguish between them. Otherwise, prefer
179+
* `subnetType`.
177180
*
178-
* At most one of `subnetType` and `subnetName` can be supplied.
181+
* This field does not select individual subnets, it selects all subnets that
182+
* share the given subnet group name. This is the name supplied in
183+
* `subnetConfiguration`.
179184
*
180-
* @default name
185+
* At most one of `subnetType` and `subnetGroupName` can be supplied.
186+
*
187+
* @default - Selection by type instead of by name
188+
*/
189+
readonly subnetGroupName?: string;
190+
191+
/**
192+
* Alias for `subnetGroupName`
193+
*
194+
* Select the subnet group with the given name. This only needs
195+
* to be used if you have multiple subnet groups of the same type
196+
* and you need to distinguish between them.
197+
*
198+
* @deprecated Use `subnetGroupName` instead
181199
*/
182200
readonly subnetName?: string;
183201

@@ -315,26 +333,45 @@ abstract class VpcBase extends Resource implements IVpc {
315333
*/
316334
protected selectSubnetObjects(selection: SubnetSelection = {}): ISubnet[] {
317335
selection = reifySelectionDefaults(selection);
318-
let subnets: ISubnet[] = [];
319-
320-
if (selection.subnetName !== undefined) { // Select by name
321-
const allSubnets = [...this.publicSubnets, ...this.privateSubnets, ...this.isolatedSubnets];
322-
subnets = allSubnets.filter(s => subnetName(s) === selection.subnetName);
323-
} else { // Select by type
324-
subnets = {
325-
[SubnetType.ISOLATED]: this.isolatedSubnets,
326-
[SubnetType.PRIVATE]: this.privateSubnets,
327-
[SubnetType.PUBLIC]: this.publicSubnets,
328-
}[selection.subnetType || SubnetType.PRIVATE];
329-
330-
if (selection.onePerAz && subnets.length > 0) {
331-
// Restrict to at most one subnet group
332-
subnets = subnets.filter(s => subnetName(s) === subnetName(subnets[0]));
333-
}
336+
337+
if (selection.subnetGroupName !== undefined) { // Select by name
338+
return this.selectSubnetObjectsByName(selection.subnetGroupName);
339+
340+
} else {
341+
const type = selection.subnetType || SubnetType.PRIVATE;
342+
return this.selectSubnetObjectsByType(type, !!selection.onePerAz);
343+
}
344+
}
345+
346+
private selectSubnetObjectsByName(groupName: string) {
347+
const allSubnets = [...this.publicSubnets, ...this.privateSubnets, ...this.isolatedSubnets];
348+
const subnets = allSubnets.filter(s => subnetGroupNameFromConstructId(s) === groupName);
349+
350+
if (subnets.length === 0) {
351+
const names = Array.from(new Set(allSubnets.map(subnetGroupNameFromConstructId)));
352+
throw new Error(`There are no subnet groups with name '${groupName}' in this VPC. Available names: ${names}`);
353+
}
354+
355+
return subnets;
356+
}
357+
358+
private selectSubnetObjectsByType(subnetType: SubnetType, onePerAz: boolean) {
359+
const allSubnets = {
360+
[SubnetType.ISOLATED]: this.isolatedSubnets,
361+
[SubnetType.PRIVATE]: this.privateSubnets,
362+
[SubnetType.PUBLIC]: this.publicSubnets,
363+
};
364+
365+
let subnets = allSubnets[subnetType];
366+
367+
if (onePerAz && subnets.length > 0) {
368+
// Restrict to at most one subnet group
369+
subnets = subnets.filter(s => subnetGroupNameFromConstructId(s) === subnetGroupNameFromConstructId(subnets[0]));
334370
}
335371

336372
if (subnets.length === 0) {
337-
throw new Error(`There are no ${describeSelection(selection)} in this VPC. Use a different VPC subnet selection.`);
373+
const availableTypes = Object.entries(allSubnets).filter(([_, subs]) => subs.length > 0).map(([typeName, _]) => typeName);
374+
throw new Error(`There are no '${subnetType}' subnet groups in this VPC. Available types: ${availableTypes}`);
338375
}
339376

340377
return subnets;
@@ -1407,30 +1444,24 @@ class ImportedVpc extends VpcBase {
14071444
* Returns "private subnets" by default.
14081445
*/
14091446
function reifySelectionDefaults(placement: SubnetSelection): SubnetSelection {
1410-
if (placement.subnetType !== undefined && placement.subnetName !== undefined) {
1411-
throw new Error('Only one of subnetType and subnetName can be supplied');
1447+
if (placement.subnetName !== undefined) {
1448+
if (placement.subnetGroupName !== undefined) {
1449+
throw new Error(`Please use only 'subnetGroupName' ('subnetName' is deprecated and has the same behavior)`);
1450+
}
1451+
placement = {...placement, subnetGroupName: placement.subnetName };
14121452
}
14131453

1414-
if (placement.subnetType === undefined && placement.subnetName === undefined) {
1454+
if (placement.subnetType !== undefined && placement.subnetGroupName !== undefined) {
1455+
throw new Error(`Only one of 'subnetType' and 'subnetGroupName' can be supplied`);
1456+
}
1457+
1458+
if (placement.subnetType === undefined && placement.subnetGroupName === undefined) {
14151459
return { subnetType: SubnetType.PRIVATE, onePerAz: placement.onePerAz };
14161460
}
14171461

14181462
return placement;
14191463
}
14201464

1421-
/**
1422-
* Describe the given placement strategy
1423-
*/
1424-
function describeSelection(placement: SubnetSelection): string {
1425-
if (placement.subnetType !== undefined) {
1426-
return `'${defaultSubnetName(placement.subnetType)}' subnets`;
1427-
}
1428-
if (placement.subnetName !== undefined) {
1429-
return `subnets named '${placement.subnetName}'`;
1430-
}
1431-
return JSON.stringify(placement);
1432-
}
1433-
14341465
class CompositeDependable implements IDependable {
14351466
private readonly dependables = new Array<IDependable>();
14361467

packages/@aws-cdk/aws-ec2/test/test.vpc.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ export = {
400400
},
401401
],
402402
natGatewaySubnets: {
403-
subnetName: 'egress'
403+
subnetGroupName: 'egress'
404404
},
405405
});
406406
expect(stack).to(countResources("AWS::EC2::NatGateway", 3));
@@ -431,7 +431,7 @@ export = {
431431
},
432432
],
433433
natGatewaySubnets: {
434-
subnetName: 'notthere',
434+
subnetGroupName: 'notthere',
435435
},
436436
}));
437437
test.done();
@@ -773,6 +773,24 @@ export = {
773773
]
774774
});
775775

776+
// WHEN
777+
const { subnetIds } = vpc.selectSubnets({ subnetGroupName: 'DontTalkToMe' });
778+
779+
// THEN
780+
test.deepEqual(subnetIds, vpc.privateSubnets.map(s => s.subnetId));
781+
test.done();
782+
},
783+
784+
'subnetName is an alias for subnetGroupName (backwards compat)'(test: Test) {
785+
// GIVEN
786+
const stack = getTestStack();
787+
const vpc = new Vpc(stack, 'VPC', {
788+
subnetConfiguration: [
789+
{ subnetType: SubnetType.PRIVATE, name: 'DontTalkToMe' },
790+
{ subnetType: SubnetType.ISOLATED, name: 'DontTalkAtAll' },
791+
]
792+
});
793+
776794
// WHEN
777795
const { subnetIds } = vpc.selectSubnets({ subnetName: 'DontTalkToMe' });
778796

@@ -793,7 +811,19 @@ export = {
793811

794812
test.throws(() => {
795813
vpc.selectSubnets();
796-
}, /There are no 'Private' subnets in this VPC/);
814+
}, /There are no 'Private' subnet groups in this VPC. Available types: Public/);
815+
816+
test.done();
817+
},
818+
819+
'selecting subnets by name fails if the name is unknown'(test: Test) {
820+
// GIVEN
821+
const stack = new Stack();
822+
const vpc = new Vpc(stack, 'VPC');
823+
824+
test.throws(() => {
825+
vpc.selectSubnets({ subnetGroupName: 'Toot' });
826+
}, /There are no subnet groups with name 'Toot' in this VPC. Available names: Public,Private/);
797827

798828
test.done();
799829
},

0 commit comments

Comments
 (0)