Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(aws-ec2): VPC properly uses complex subnet config #610

Merged
merged 10 commits into from
Sep 9, 2018
2 changes: 1 addition & 1 deletion examples/cdk-examples-typescript/neptune-demo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class NeptuneDemoStack extends cdk.Stack {
instanceProps: {
instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small),
vpc,
vpcPlacement: { usePublicSubnets: true },
vpcPlacement: { subnetsToUse: ec2.SubnetType.Public },
},
masterUser: {
// This would normally be imported from SSM parmeter store encrypted string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,5 +242,6 @@ function mockVpc(stack: cdk.Stack) {
availabilityZones: [ 'az1' ],
publicSubnetIds: [ new ec2.VpcSubnetId('pub1') ],
privateSubnetIds: [ new ec2.VpcSubnetId('pri1') ],
isolatedSubnetIds: [],
});
}
155 changes: 155 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import cdk = require('@aws-cdk/cdk');
import { SubnetType, VpcSubnetRef } from "./vpc-ref";

/**
* Turn an arbitrary string into one that can be used as a CloudFormation identifier by stripping special characters
*
Expand All @@ -6,3 +9,155 @@
export function slugify(x: string): string {
return x.replace(/[^a-zA-Z0-9]/g, '');
}

/**
* The default names for every subnet type
*/
export const DEFAULT_SUBNET_NAME = {
[SubnetType.Public]: 'Public',
[SubnetType.Private]: 'Private',
[SubnetType.Isolated]: 'Isolated',
};

/**
* Return a subnet name from its construct ID
*
* All subnet names look like NAME <> "Subnet" <> INDEX
*/
export function subnetName(subnet: VpcSubnetRef) {
return subnet.id.replace(/Subnet\d+$/, '');
}

/**
* Make the subnet construct ID from a name and number
*/
export function subnetId(name: string, i: number) {
return `${name}Subnet${i + 1}`;
}

/**
* Helper class to export/import groups of subnets
*/
export class ExportSubnetGroup {
public readonly ids?: cdk.Token[];
public readonly names?: string[];

private readonly groups: number;

constructor(
parent: cdk.Construct,
exportName: string,
private readonly subnets: VpcSubnetRef[],
private readonly type: SubnetType,
private readonly azs: number) {

this.groups = subnets.length / azs;

// ASSERTION
if (Math.floor(this.groups) !== this.groups) {
throw new Error(`Number of subnets (${subnets.length}) must be a multiple of number of availability zones (${azs})`);
}

this.ids = this.exportIds(parent, exportName);
this.names = this.exportNames();
}

private exportIds(parent: cdk.Construct, name: string): cdk.Token[] | undefined {
if (this.subnets.length === 0) { return undefined; }
return new cdk.StringListOutput(parent, name, { values: this.subnets.map(s => s.subnetId) }).makeImportValues();
}

/**
* Return the list of subnet names if they're not equal to the default
*/
private exportNames(): string[] | undefined {
if (this.subnets.length === 0) { return undefined; }
const netNames = this.subnets.map(subnetName);

// Do some assertion that the 'netNames' array is laid out like this:
//
// [ INGRESS, INGRESS, INGRESS, EGRESS, EGRESS, EGRESS, ... ]
for (let i = 0; i < netNames.length; i++) {
const k = Math.floor(i / this.azs);
if (netNames[i] !== netNames[k * this.azs]) {
throw new Error(`Subnets must be grouped by name, got: ${JSON.stringify(netNames)}`);
}
}

// Splat down to [ INGRESS, EGRESS, ... ]
const groupNames = range(this.groups).map(i => netNames[i * this.azs]);
if (groupNames.length === 1 && groupNames[0] === DEFAULT_SUBNET_NAME[this.type]) { return undefined; }

return groupNames;
}
}

export class ImportSubnetGroup {
private readonly subnetIds: cdk.Token[];
private readonly names: string[];
private readonly groups: number;

constructor(
subnetIds: cdk.Token[] | undefined,
names: string[] | undefined,
type: SubnetType,
private readonly availabilityZones: string[],
idField: string,
nameField: string) {

this.subnetIds = subnetIds || [];
this.groups = this.subnetIds.length / this.availabilityZones.length;

if (Math.floor(this.groups) !== this.groups) {
// tslint:disable-next-line:max-line-length
throw new Error(`Amount of ${idField} (${this.subnetIds.length}) must be a multiple of availability zones (${this.availabilityZones.length}).`);
}

this.names = this.normalizeNames(names, DEFAULT_SUBNET_NAME[type], nameField);
}

public import(parent: cdk.Construct): VpcSubnetRef[] {
return range(this.subnetIds.length).map(i => {
const k = Math.floor(i / this.availabilityZones.length);
return VpcSubnetRef.import(parent, subnetId(this.names[k], i), {
availabilityZone: this.pickAZ(i),
subnetId: this.subnetIds[i]
});
});
}

/**
* Return a list with a name for every subnet
*/
private normalizeNames(names: string[] | undefined, defaultName: string, fieldName: string) {
// If not given, return default
if (names === undefined || names.length === 0) {
return [defaultName];
}

// If given, must match given subnets
if (names.length !== this.groups) {
throw new Error(`${fieldName} must have an entry for every corresponding subnet group, got: ${JSON.stringify(names)}`);
}

return names;
}

/**
* Return the i'th AZ
*/
private pickAZ(i: number) {
return this.availabilityZones[i % this.availabilityZones.length];
}
}

/**
* Generate the list of numbers of [0..n)
*/
export function range(n: number): number[] {
const ret: number[] = [];
for (let i = 0; i < n; i++) {
ret.push(i);
}
return ret;
}
Loading