Skip to content

Commit 54599e5

Browse files
authored
fix(vpc): recognize Public subnets by Internet Gateway (#3784)
The VPC importer used to look at `MapPublicIpOnLaunch` as a shortcut for determining whether a Subnet was public or private. The correct way to do it is to look at the route table and see if it includes an Internet Gateway. Fixes #3706.
1 parent 4afd40e commit 54599e5

File tree

2 files changed

+151
-72
lines changed

2 files changed

+151
-72
lines changed

packages/aws-cdk/lib/context-providers/vpcs.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ export class VpcNetworkContextProviderPlugin implements ContextProviderPlugin {
4747
const listedSubnets = subnetsResponse.Subnets || [];
4848

4949
const routeTablesResponse = await ec2.describeRouteTables(filters).promise();
50-
const listedRouteTables = routeTablesResponse.RouteTables || [];
51-
52-
const mainRouteTable = listedRouteTables.find(table => table.Associations && !!table.Associations.find(assoc => assoc.Main));
50+
const routeTables = new RouteTables(routeTablesResponse.RouteTables || []);
5351

5452
// Now comes our job to separate these subnets out into AZs and subnet groups (Public, Private, Isolated)
5553
// We have the following attributes to go on:
@@ -64,19 +62,17 @@ export class VpcNetworkContextProviderPlugin implements ContextProviderPlugin {
6462

6563
const subnets: Subnet[] = listedSubnets.map(subnet => {
6664
let type = getTag('aws-cdk:subnet-type', subnet.Tags);
67-
if (type === undefined) {
68-
type = subnet.MapPublicIpOnLaunch ? SubnetType.Public : SubnetType.Private;
69-
}
65+
if (type === undefined && subnet.MapPublicIpOnLaunch) { type = SubnetType.Public; }
66+
if (type === undefined && routeTables.hasRouteToIgw(subnet.SubnetId)) { type = SubnetType.Public; }
67+
if (type === undefined) { type = SubnetType.Private; }
68+
7069
if (!isValidSubnetType(type)) {
7170
// tslint:disable-next-line: max-line-length
7271
throw new Error(`Subnet ${subnet.SubnetArn} has invalid subnet type ${type} (must be ${SubnetType.Public}, ${SubnetType.Private} or ${SubnetType.Isolated})`);
7372
}
7473

7574
const name = getTag('aws-cdk:subnet-name', subnet.Tags) || type;
76-
const specificRouteTable =
77-
listedRouteTables.find(table => table.Associations && !!table.Associations.find(assoc => assoc.SubnetId === subnet.SubnetId));
78-
const routeTableId = (specificRouteTable && specificRouteTable.RouteTableId)
79-
|| (mainRouteTable && mainRouteTable.RouteTableId);
75+
const routeTableId = routeTables.routeTableIdForSubnetId(subnet.SubnetId);
8076

8177
if (!routeTableId) {
8278
throw new Error(`Subnet ${subnet.SubnetArn} does not have an associated route table (and there is no "main" table)`);
@@ -131,6 +127,32 @@ export class VpcNetworkContextProviderPlugin implements ContextProviderPlugin {
131127
}
132128
}
133129

130+
class RouteTables {
131+
public readonly mainRouteTable?: AWS.EC2.RouteTable;
132+
133+
constructor(private readonly tables: AWS.EC2.RouteTable[]) {
134+
this.mainRouteTable = this.tables.find(table => !!table.Associations && table.Associations.some(assoc => !!assoc.Main));
135+
}
136+
137+
public routeTableIdForSubnetId(subnetId: string | undefined): string | undefined {
138+
const table = this.tableForSubnet(subnetId);
139+
return (table && table.RouteTableId) || (this.mainRouteTable && this.mainRouteTable.RouteTableId);
140+
}
141+
142+
/**
143+
* Whether the given subnet has a route to an IGW
144+
*/
145+
public hasRouteToIgw(subnetId: string | undefined): boolean {
146+
const table = this.tableForSubnet(subnetId);
147+
148+
return !!table && !!table.Routes && table.Routes.some(route => !!route.GatewayId && route.GatewayId.startsWith('igw-'));
149+
}
150+
151+
public tableForSubnet(subnetId: string | undefined) {
152+
return this.tables.find(table => !!table.Associations && table.Associations.some(assoc => assoc.SubnetId === subnetId));
153+
}
154+
}
155+
134156
/**
135157
* Return the value of a tag from a set of tags
136158
*/

packages/aws-cdk/test/context-providers/test.vpcs.ts

Lines changed: 119 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -25,35 +25,17 @@ export = nodeunit.testCase({
2525
const filter = { foo: 'bar' };
2626
const provider = new VpcNetworkContextProviderPlugin(mockSDK);
2727

28-
AWS.mock('EC2', 'describeVpcs', (params: aws.EC2.DescribeVpcsRequest, cb: AwsCallback<aws.EC2.DescribeVpcsResult>) => {
29-
test.deepEqual(params.Filters, [{ Name: 'foo', Values: ['bar'] }]);
30-
return cb(null, { Vpcs: [{ VpcId: 'vpc-1234567' }] });
31-
});
32-
AWS.mock('EC2', 'describeSubnets', (params: aws.EC2.DescribeSubnetsRequest, cb: AwsCallback<aws.EC2.DescribeSubnetsResult>) => {
33-
test.deepEqual(params.Filters, [{ Name: 'vpc-id', Values: ['vpc-1234567'] }]);
34-
return cb(null, {
35-
Subnets: [
36-
{ SubnetId: 'sub-123456', AvailabilityZone: 'bermuda-triangle-1337', MapPublicIpOnLaunch: true },
37-
{ SubnetId: 'sub-789012', AvailabilityZone: 'bermuda-triangle-1337', MapPublicIpOnLaunch: false }
38-
]
39-
});
40-
});
41-
AWS.mock('EC2', 'describeRouteTables', (params: aws.EC2.DescribeRouteTablesRequest, cb: AwsCallback<aws.EC2.DescribeRouteTablesResult>) => {
42-
test.deepEqual(params.Filters, [{ Name: 'vpc-id', Values: ['vpc-1234567'] }]);
43-
return cb(null, {
44-
RouteTables: [
45-
{ Associations: [{ SubnetId: 'sub-123456' }], RouteTableId: 'rtb-123456', },
46-
{ Associations: [{ SubnetId: 'sub-789012' }], RouteTableId: 'rtb-789012', }
47-
]
48-
});
49-
});
50-
AWS.mock('EC2', 'describeVpnGateways', (params: aws.EC2.DescribeVpnGatewaysRequest, cb: AwsCallback<aws.EC2.DescribeVpnGatewaysResult>) => {
51-
test.deepEqual(params.Filters, [
52-
{ Name: 'attachment.vpc-id', Values: [ 'vpc-1234567' ] },
53-
{ Name: 'attachment.state', Values: [ 'attached' ] },
54-
{ Name: 'state', Values: [ 'available' ] }
55-
]);
56-
return cb(null, { VpnGateways: [{ VpnGatewayId: 'gw-abcdef' }] });
28+
mockVpcLookup(test, {
29+
subnets: [
30+
{ SubnetId: 'sub-123456', AvailabilityZone: 'bermuda-triangle-1337', MapPublicIpOnLaunch: true },
31+
{ SubnetId: 'sub-789012', AvailabilityZone: 'bermuda-triangle-1337', MapPublicIpOnLaunch: false }
32+
],
33+
routeTables: [
34+
{ Associations: [{ SubnetId: 'sub-123456' }], RouteTableId: 'rtb-123456', },
35+
{ Associations: [{ SubnetId: 'sub-789012' }], RouteTableId: 'rtb-789012', }
36+
],
37+
vpnGateways: [{ VpnGatewayId: 'gw-abcdef' }]
38+
5739
});
5840

5941
// WHEN
@@ -75,8 +57,8 @@ export = nodeunit.testCase({
7557
vpnGatewayId: 'gw-abcdef'
7658
});
7759

78-
test.done();
7960
AWS.restore();
61+
test.done();
8062
},
8163

8264
async 'throws when no such VPC is found'(test: nodeunit.Test) {
@@ -97,8 +79,8 @@ export = nodeunit.testCase({
9779
test.throws(() => { throw e; }, /Could not find any VPCs matching/);
9880
}
9981

100-
test.done();
10182
AWS.restore();
83+
test.done();
10284
},
10385

10486
async 'throws when multiple VPCs are found'(test: nodeunit.Test) {
@@ -119,44 +101,25 @@ export = nodeunit.testCase({
119101
test.throws(() => { throw e; }, /Found 2 VPCs matching/);
120102
}
121103

122-
test.done();
123104
AWS.restore();
105+
test.done();
124106
},
125107

126108
async 'uses the VPC main route table when a subnet has no specific association'(test: nodeunit.Test) {
127109
// GIVEN
128110
const filter = { foo: 'bar' };
129111
const provider = new VpcNetworkContextProviderPlugin(mockSDK);
130112

131-
AWS.mock('EC2', 'describeVpcs', (params: aws.EC2.DescribeVpcsRequest, cb: AwsCallback<aws.EC2.DescribeVpcsResult>) => {
132-
test.deepEqual(params.Filters, [{ Name: 'foo', Values: ['bar'] }]);
133-
return cb(null, { Vpcs: [{ VpcId: 'vpc-1234567' }] });
134-
});
135-
AWS.mock('EC2', 'describeSubnets', (params: aws.EC2.DescribeSubnetsRequest, cb: AwsCallback<aws.EC2.DescribeSubnetsResult>) => {
136-
test.deepEqual(params.Filters, [{ Name: 'vpc-id', Values: ['vpc-1234567'] }]);
137-
return cb(null, {
138-
Subnets: [
139-
{ SubnetId: 'sub-123456', AvailabilityZone: 'bermuda-triangle-1337', MapPublicIpOnLaunch: true },
140-
{ SubnetId: 'sub-789012', AvailabilityZone: 'bermuda-triangle-1337', MapPublicIpOnLaunch: false }
141-
]
142-
});
143-
});
144-
AWS.mock('EC2', 'describeRouteTables', (params: aws.EC2.DescribeRouteTablesRequest, cb: AwsCallback<aws.EC2.DescribeRouteTablesResult>) => {
145-
test.deepEqual(params.Filters, [{ Name: 'vpc-id', Values: ['vpc-1234567'] }]);
146-
return cb(null, {
147-
RouteTables: [
148-
{ Associations: [{ SubnetId: 'sub-123456' }], RouteTableId: 'rtb-123456', },
149-
{ Associations: [{ Main: true }], RouteTableId: 'rtb-789012', }
150-
]
151-
});
152-
});
153-
AWS.mock('EC2', 'describeVpnGateways', (params: aws.EC2.DescribeVpnGatewaysRequest, cb: AwsCallback<aws.EC2.DescribeVpnGatewaysResult>) => {
154-
test.deepEqual(params.Filters, [
155-
{ Name: 'attachment.vpc-id', Values: [ 'vpc-1234567' ] },
156-
{ Name: 'attachment.state', Values: [ 'attached' ] },
157-
{ Name: 'state', Values: [ 'available' ] }
158-
]);
159-
return cb(null, { VpnGateways: [{ VpnGatewayId: 'gw-abcdef' }] });
113+
mockVpcLookup(test, {
114+
subnets: [
115+
{ SubnetId: 'sub-123456', AvailabilityZone: 'bermuda-triangle-1337', MapPublicIpOnLaunch: true },
116+
{ SubnetId: 'sub-789012', AvailabilityZone: 'bermuda-triangle-1337', MapPublicIpOnLaunch: false }
117+
],
118+
routeTables: [
119+
{ Associations: [{ SubnetId: 'sub-123456' }], RouteTableId: 'rtb-123456', },
120+
{ Associations: [{ Main: true }], RouteTableId: 'rtb-789012', }
121+
],
122+
vpnGateways: [{ VpnGatewayId: 'gw-abcdef' }]
160123
});
161124

162125
// WHEN
@@ -180,5 +143,99 @@ export = nodeunit.testCase({
180143

181144
test.done();
182145
AWS.restore();
183-
}
146+
},
147+
148+
async 'Recognize public subnet by route table'(test: nodeunit.Test) {
149+
// GIVEN
150+
const filter = { foo: 'bar' };
151+
const provider = new VpcNetworkContextProviderPlugin(mockSDK);
152+
153+
mockVpcLookup(test, {
154+
subnets: [
155+
{ SubnetId: 'sub-123456', AvailabilityZone: 'bermuda-triangle-1337', MapPublicIpOnLaunch: false },
156+
],
157+
routeTables: [
158+
{
159+
Associations: [{ SubnetId: 'sub-123456' }],
160+
RouteTableId: 'rtb-123456',
161+
Routes: [
162+
{
163+
DestinationCidrBlock: "10.0.2.0/26",
164+
Origin: "CreateRoute",
165+
State: "active",
166+
VpcPeeringConnectionId: "pcx-xxxxxx"
167+
},
168+
{
169+
DestinationCidrBlock: "10.0.1.0/24",
170+
GatewayId: "local",
171+
Origin: "CreateRouteTable",
172+
State: "active"
173+
},
174+
{
175+
DestinationCidrBlock: "0.0.0.0/0",
176+
GatewayId: "igw-xxxxxx",
177+
Origin: "CreateRoute",
178+
State: "active"
179+
}
180+
],
181+
},
182+
],
183+
});
184+
185+
// WHEN
186+
const result = await provider.getValue({ filter });
187+
188+
// THEN
189+
test.deepEqual(result, {
190+
vpcId: 'vpc-1234567',
191+
availabilityZones: ['bermuda-triangle-1337'],
192+
isolatedSubnetIds: undefined,
193+
isolatedSubnetNames: undefined,
194+
isolatedSubnetRouteTableIds: undefined,
195+
privateSubnetIds: undefined,
196+
privateSubnetNames: undefined,
197+
privateSubnetRouteTableIds: undefined,
198+
publicSubnetIds: ['sub-123456'],
199+
publicSubnetNames: ['Public'],
200+
publicSubnetRouteTableIds: ['rtb-123456'],
201+
vpnGatewayId: undefined,
202+
});
203+
204+
AWS.restore();
205+
test.done();
206+
},
184207
});
208+
209+
interface VpcLookupOptions {
210+
subnets: aws.EC2.Subnet[];
211+
routeTables: aws.EC2.RouteTable[];
212+
vpnGateways?: aws.EC2.VpnGateway[];
213+
}
214+
215+
function mockVpcLookup(test: nodeunit.Test, options: VpcLookupOptions) {
216+
const VpcId = 'vpc-1234567';
217+
218+
AWS.mock('EC2', 'describeVpcs', (params: aws.EC2.DescribeVpcsRequest, cb: AwsCallback<aws.EC2.DescribeVpcsResult>) => {
219+
test.deepEqual(params.Filters, [{ Name: 'foo', Values: ['bar'] }]);
220+
return cb(null, { Vpcs: [{ VpcId }] });
221+
});
222+
223+
AWS.mock('EC2', 'describeSubnets', (params: aws.EC2.DescribeSubnetsRequest, cb: AwsCallback<aws.EC2.DescribeSubnetsResult>) => {
224+
test.deepEqual(params.Filters, [{ Name: 'vpc-id', Values: [VpcId] }]);
225+
return cb(null, { Subnets: options.subnets });
226+
});
227+
228+
AWS.mock('EC2', 'describeRouteTables', (params: aws.EC2.DescribeRouteTablesRequest, cb: AwsCallback<aws.EC2.DescribeRouteTablesResult>) => {
229+
test.deepEqual(params.Filters, [{ Name: 'vpc-id', Values: [VpcId] }]);
230+
return cb(null, { RouteTables: options.routeTables });
231+
});
232+
233+
AWS.mock('EC2', 'describeVpnGateways', (params: aws.EC2.DescribeVpnGatewaysRequest, cb: AwsCallback<aws.EC2.DescribeVpnGatewaysResult>) => {
234+
test.deepEqual(params.Filters, [
235+
{ Name: 'attachment.vpc-id', Values: [ VpcId ] },
236+
{ Name: 'attachment.state', Values: [ 'attached' ] },
237+
{ Name: 'state', Values: [ 'available' ] }
238+
]);
239+
return cb(null, { VpnGateways: options.vpnGateways });
240+
});
241+
}

0 commit comments

Comments
 (0)