diff --git a/packages/@aws-cdk/aws-vpcv2-alpha/lib/ipam.ts b/packages/@aws-cdk/aws-vpcv2-alpha/lib/ipam.ts index 48eef25123f90..fe8d6a0cadd61 100644 --- a/packages/@aws-cdk/aws-vpcv2-alpha/lib/ipam.ts +++ b/packages/@aws-cdk/aws-vpcv2-alpha/lib/ipam.ts @@ -106,8 +106,8 @@ export class Ipam { public readonly ipamID: string; constructor(scope: Construct, id: string) { this._ipam = new CfnIPAM(scope, id); - this.publicScope = new IpamPublicScope(scope, 'DefaultPublic'); - this.privateScope = new IpamPrivateScope(scope, 'DefaultPrivate'); + this.publicScope = new IpamPublicScope(scope, this._ipam.attrPublicDefaultScopeId); + this.privateScope = new IpamPrivateScope(scope, this._ipam.attrPrivateDefaultScopeId); this.ipamID = this._ipam.attrIpamId; } } @@ -180,28 +180,6 @@ export class IpamPrivateScope { // provisionedCidrs: [{ cidr: '10.0.0.0/24' }], // }); -// export class IpamPool extends Resource { - -// static publicScope: any; - -// publicScope = { -// addPool: (options: PoolOptions) => { -// const scope = new IpamScope(this, 'PublicScope', { -// scopeType: ScopeType.PUBLIC, -// }); -// return new CfnIPAMPool(this, '', { -// addressFamily: options.addressFamily.toString(), -// provisionedCidrs: options.provisionedCidrs, -// ipamScopeId: scope.ipamScopeId; -// }); -// }, -// } -// constructor(stack: Construct, id: string) { -// super(stack, id); -// } - -// } - export class IpamIpv6 implements IIpAddresses { constructor(private readonly props: IpamOptions) { diff --git a/packages/@aws-cdk/aws-vpcv2-alpha/lib/subnet-v2.ts b/packages/@aws-cdk/aws-vpcv2-alpha/lib/subnet-v2.ts index b361872dc2afa..01f1a9de57437 100644 --- a/packages/@aws-cdk/aws-vpcv2-alpha/lib/subnet-v2.ts +++ b/packages/@aws-cdk/aws-vpcv2-alpha/lib/subnet-v2.ts @@ -1,8 +1,9 @@ /* eslint-disable @typescript-eslint/member-ordering */ // /* eslint-disable @typescript-eslint/member-ordering */ import { Resource, Names } from 'aws-cdk-lib'; -import { CfnRouteTable, CfnSubnet, CfnSubnetRouteTableAssociation, INetworkAcl, IRouteTable, ISubnet, IVpc, NetworkAcl, SubnetNetworkAclAssociation } from 'aws-cdk-lib/aws-ec2'; +import { CfnRouteTable, CfnSubnet, CfnSubnetRouteTableAssociation, INetworkAcl, IRouteTable, ISubnet, NetworkAcl, SubnetNetworkAclAssociation } from 'aws-cdk-lib/aws-ec2'; import { Construct, DependencyGroup, IDependable } from 'constructs'; +import { IVpcV2 } from './vpc-v2-base'; export interface ICidr { readonly cidr: string; @@ -23,8 +24,8 @@ export class Ipv4Cidr implements ICidr { export class Ipv6Cidr implements ICidr { public readonly cidr: string; - constructor(props: cidrBlockProps ) { - this.cidr = props.cidrBlock; + constructor(props: string ) { + this.cidr = props; } } @@ -32,7 +33,7 @@ export interface SubnetPropsV2 { /** * VPC Prop */ - vpc: IVpc; + vpc: IVpcV2; /** * custom CIDR range @@ -104,18 +105,21 @@ export class SubnetV2 extends Resource implements ISubnet { constructor(scope: Construct, id: string, props: SubnetPropsV2) { super(scope, id); - let ipv4cidr: string = ''; - //let ipv6cidr: string = ''; + let ipv4CidrBlock: string | undefined; + let ipv6CidrBlock: string| undefined; + if (props.cidrBlock instanceof Ipv4Cidr) { - ipv4cidr = props.cidrBlock.cidr; + ipv4CidrBlock = props.cidrBlock.cidr; + } else if (props.cidrBlock instanceof Ipv6Cidr) { + if (validateSupportIpv6(props.vpc)) { + ipv6CidrBlock = props.cidrBlock.cidr; + } + } - // } else if (props.cidrBlock instanceof Ipv6Cidr) { - // ipv6cidr = props.cidrBlock.cidr; - // } const subnet = new CfnSubnet(this, 'Subnet', { vpcId: props.vpc.vpcId, - cidrBlock: ipv4cidr, - //ipv6CidrBlock: ipv6cidr, + cidrBlock: ipv4CidrBlock, + ipv6CidrBlock: ipv6CidrBlock, availabilityZone: props.availabilityZone, }); @@ -150,6 +154,7 @@ export class SubnetV2 extends Resource implements ISubnet { pushIsolatedSubnet(props.vpc, this); } //isolated by default } + /** * Associate a Network ACL with this subnet * @@ -172,6 +177,18 @@ export class SubnetV2 extends Resource implements ISubnet { } -function pushIsolatedSubnet(vpc: IVpc, subnet: SubnetV2) { +function pushIsolatedSubnet(vpc: IVpcV2, subnet: SubnetV2) { vpc.isolatedSubnets.push(subnet); +} + +/** + * currently checking for amazon provided Ipv6 only which we plan to release + */ + +function validateSupportIpv6(vpc: IVpcV2) { + if (vpc.cidrBlock.some((secondaryAddress) => secondaryAddress.amazonProvidedIpv6CidrBlock === true)) { + return true; + } else { + throw new Error('To use IPv6, the VPC must enable IPv6 support.'); + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-vpcv2-alpha/lib/util.ts b/packages/@aws-cdk/aws-vpcv2-alpha/lib/util.ts new file mode 100644 index 0000000000000..ca30abaa0c1e5 --- /dev/null +++ b/packages/@aws-cdk/aws-vpcv2-alpha/lib/util.ts @@ -0,0 +1,150 @@ +import { Construct } from 'constructs'; +import { ISubnet, Subnet, SubnetType } from 'aws-cdk-lib/aws-ec2'; + +/** + * Turn an arbitrary string into one that can be used as a CloudFormation identifier by stripping special characters + * + * (At the moment, no efforts are taken to prevent collisions, but we can add that later when it becomes necessary). + */ +export function slugify(x: string): string { + return x.replace(/[^a-zA-Z0-9]/g, ''); +} + +/** + * The default names for every subnet type + */ +export function defaultSubnetName(type: SubnetType) { + switch (type) { + case SubnetType.PUBLIC: return 'Public'; + case SubnetType.PRIVATE_WITH_NAT: + case SubnetType.PRIVATE_WITH_EGRESS: + case SubnetType.PRIVATE: + return 'Private'; + case SubnetType.PRIVATE_ISOLATED: + case SubnetType.ISOLATED: + return 'Isolated'; + } +} + +/** + * Return a subnet name from its construct ID + * + * All subnet names look like NAME <> "Subnet" <> INDEX + */ +export function subnetGroupNameFromConstructId(subnet: ISubnet) { + return subnet.node.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}`; +} + +export class ImportSubnetGroup { + private readonly subnetIds: string[]; + private readonly names: string[]; + private readonly routeTableIds: string[]; + private readonly ipv4CidrBlocks: string[]; + private readonly groups: number; + + constructor( + subnetIds: string[] | undefined, + names: string[] | undefined, + routeTableIds: string[] | undefined, + ipv4CidrBlocks: string[] | undefined, + type: SubnetType, + private readonly availabilityZones: string[], + idField: string, + nameField: string, + routeTableIdField: string, + ipv4CidrBlockField: string) { + + this.subnetIds = subnetIds || []; + this.routeTableIds = routeTableIds || []; + this.ipv4CidrBlocks = ipv4CidrBlocks || []; + this.groups = this.subnetIds.length / this.availabilityZones.length; + + if (Math.floor(this.groups) !== this.groups) { + // eslint-disable-next-line max-len + throw new Error(`Number of ${idField} (${this.subnetIds.length}) must be a multiple of availability zones (${this.availabilityZones.length}).`); + } + if (this.routeTableIds.length !== this.subnetIds.length && routeTableIds != null) { + // We don't err if no routeTableIds were provided to maintain backwards-compatibility. See https://github.com/aws/aws-cdk/pull/3171 + /* eslint-disable max-len */ + throw new Error(`Number of ${routeTableIdField} (${this.routeTableIds.length}) must be equal to the amount of ${idField} (${this.subnetIds.length}).`); + } + if (this.ipv4CidrBlocks.length !== this.subnetIds.length && ipv4CidrBlocks != null) { + // We don't err if no ipv4CidrBlocks were provided to maintain backwards-compatibility. + /* eslint-disable max-len */ + throw new Error(`Number of ${ipv4CidrBlockField} (${this.ipv4CidrBlocks.length}) must be equal to the amount of ${idField} (${this.subnetIds.length}).`); + } + + this.names = this.normalizeNames(names, defaultSubnetName(type), nameField); + } + + public import(scope: Construct): ISubnet[] { + return range(this.subnetIds.length).map(i => { + const k = Math.floor(i / this.availabilityZones.length); + return Subnet.fromSubnetAttributes(scope, subnetId(this.names[k], i), { + availabilityZone: this.pickAZ(i), + subnetId: this.subnetIds[i], + routeTableId: this.routeTableIds[i], + ipv4CidrBlock: this.ipv4CidrBlocks[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; +} + +/** + * Return the union of table IDs from all selected subnets + */ +export function allRouteTableIds(subnets: ISubnet[]): string[] { + const ret = new Set(); + for (const subnet of subnets) { + if (subnet.routeTable && subnet.routeTable.routeTableId) { + ret.add(subnet.routeTable.routeTableId); + } + } + return Array.from(ret); +} + +export function flatten(xs: A[][]): A[] { + return Array.prototype.concat.apply([], xs); +} diff --git a/packages/@aws-cdk/aws-vpcv2-alpha/lib/vpc-v2-base.ts b/packages/@aws-cdk/aws-vpcv2-alpha/lib/vpc-v2-base.ts index d3d403eace1a6..971ae3c1b10ba 100644 --- a/packages/@aws-cdk/aws-vpcv2-alpha/lib/vpc-v2-base.ts +++ b/packages/@aws-cdk/aws-vpcv2-alpha/lib/vpc-v2-base.ts @@ -1,9 +1,18 @@ import { Resource, Annotations } from 'aws-cdk-lib'; -import { IVpc, ISubnet, SubnetSelection, SelectedSubnets, EnableVpnGatewayOptions, VpnGateway, VpnConnectionType, CfnVPCGatewayAttachment, CfnVPNGatewayRoutePropagation, VpnConnectionOptions, VpnConnection, ClientVpnEndpointOptions, ClientVpnEndpoint, InterfaceVpcEndpointOptions, InterfaceVpcEndpoint, GatewayVpcEndpointOptions, GatewayVpcEndpoint, FlowLogOptions, FlowLog, FlowLogResourceType, SubnetType, SubnetFilter } from 'aws-cdk-lib/aws-ec2'; -import { allRouteTableIds, flatten, subnetGroupNameFromConstructId } from 'aws-cdk-lib/aws-ec2/lib/util'; +import { IVpc, ISubnet, SubnetSelection, SelectedSubnets, EnableVpnGatewayOptions, VpnGateway, VpnConnectionType, CfnVPCGatewayAttachment, CfnVPNGatewayRoutePropagation, VpnConnectionOptions, VpnConnection, ClientVpnEndpointOptions, ClientVpnEndpoint, InterfaceVpcEndpointOptions, InterfaceVpcEndpoint, GatewayVpcEndpointOptions, GatewayVpcEndpoint, FlowLogOptions, FlowLog, FlowLogResourceType, SubnetType, SubnetFilter, CfnVPCCidrBlock } from 'aws-cdk-lib/aws-ec2'; +// eslint-disable-next-line no-duplicate-imports +import { allRouteTableIds, flatten, subnetGroupNameFromConstructId } from '../lib/util'; import { IDependable, Dependable, IConstruct } from 'constructs'; -export abstract class VpcV2Base extends Resource implements IVpc { +/** + * Placeholder to see what extra props we might need, + * will be added to original IVPC + */ +export interface IVpcV2 extends IVpc { + readonly cidrBlock: CfnVPCCidrBlock[]; +} + +export abstract class VpcV2Base extends Resource implements IVpcV2 { /** * Identifier for this VPC @@ -50,6 +59,8 @@ export abstract class VpcV2Base extends Resource implements IVpc { */ public abstract readonly internetConnectivityEstablished: IDependable; + public abstract readonly cidrBlock: CfnVPCCidrBlock[]; + /** * If this is set to true, don't error out on trying to select subnets */ diff --git a/packages/@aws-cdk/aws-vpcv2-alpha/lib/vpc-v2.ts b/packages/@aws-cdk/aws-vpcv2-alpha/lib/vpc-v2.ts index 787d845c01700..f8ff903a82939 100644 --- a/packages/@aws-cdk/aws-vpcv2-alpha/lib/vpc-v2.ts +++ b/packages/@aws-cdk/aws-vpcv2-alpha/lib/vpc-v2.ts @@ -88,7 +88,7 @@ export interface VpcV2Options { readonly ipv6Pool?: string; /** - * required with cidr block for BYOL IP + * use amazon provided IP range */ readonly amazonProvided?: boolean; } @@ -144,7 +144,6 @@ export interface VpcV2Props { readonly secondaryAddressBlocks?: IIpAddresses[]; readonly enableDnsHostnames?: boolean; readonly enableDnsSupport?: boolean; - readonly useIpv6?: boolean; } /** @@ -199,6 +198,17 @@ export class VpcV2 extends VpcV2Base { private readonly _internetConnectivityEstablished = new DependencyGroup(); + /** + * reference to all secondary blocks attached + */ + public readonly cidrBlock= new Array; + + /** + * For validation to define IPv6 subnets + * @default false + */ + public readonly useIpv6: boolean = false; + constructor(scope: Construct, id: string, props: VpcV2Props = {}) { super(scope, id); @@ -226,17 +236,21 @@ export class VpcV2 extends VpcV2Base { if (props.secondaryAddressBlocks) { const secondaryAddressBlocks: IIpAddresses[] = props.secondaryAddressBlocks; - + let ipCount = 0; for (const secondaryAddressBlock of secondaryAddressBlocks) { + //TODO: Add unique has for each secondary ip address + ipCount+=1; const vpcoptions: VpcV2Options = secondaryAddressBlock.allocateVpcCidr(); + if (vpcOptions.amazonProvided === true) { + this.useIpv6 = true; + } // validate CIDR ranges per RFC 1918 // if (vpcOptions.ipv4CidrBlock!) { // validateIpv4address(vpcoptions.ipv4CidrBlock); // } //Create secondary blocks for Ipv4 and Ipv6 - //TODO: Add unique has for each secondary ip address - new CfnVPCCidrBlock(this, `Secondary${vpcoptions.ipv4CidrBlock}`, { + this.cidrBlock = [...this.cidrBlock, new CfnVPCCidrBlock(this, `SecondaryIp${ipCount}`, { vpcId: this.vpcId, cidrBlock: vpcoptions.ipv4CidrBlock, ipv4IpamPoolId: vpcoptions.ipv4IpamPoolId, @@ -247,7 +261,7 @@ export class VpcV2 extends VpcV2Base { ipv6NetmaskLength: vpcoptions.ipv6NetmaskLength, ipv6IpamPoolId: vpcoptions.ipv6IpamPoolId, amazonProvidedIpv6CidrBlock: vpcoptions.amazonProvided, - }); + })]; } } @@ -295,14 +309,14 @@ class ipv6CidrAllocation implements IIpAddresses { export class AmazonProvided implements IIpAddresses { - private readonly amazonProvided: boolean; + //private readonly amazonProvided: boolean; constructor() { - this.amazonProvided = true; + //this.amazonProvided = true; }; allocateVpcCidr(): VpcV2Options { return { - amazonProvided: this.amazonProvided, + amazonProvided: true, }; } diff --git a/packages/@aws-cdk/aws-vpcv2-alpha/test/integ.vpc-v2-alpha.ts b/packages/@aws-cdk/aws-vpcv2-alpha/test/integ.vpc-v2-alpha.ts index d214bcb54c494..7525498ae1362 100644 --- a/packages/@aws-cdk/aws-vpcv2-alpha/test/integ.vpc-v2-alpha.ts +++ b/packages/@aws-cdk/aws-vpcv2-alpha/test/integ.vpc-v2-alpha.ts @@ -12,7 +12,7 @@ import * as vpc_v2 from '../lib/vpc-v2'; import { AddressFamily, Ipam } from '../lib'; import { IntegTest } from '@aws-cdk/integ-tests-alpha'; import * as cdk from 'aws-cdk-lib'; -import { Ipv4Cidr, SubnetV2 } from '../lib/subnet-v2'; +import { Ipv6Cidr, SubnetV2 } from '../lib/subnet-v2'; // as in unit tests, we use a qualified import, // not bring in individual classes @@ -27,6 +27,7 @@ const ipam = new Ipam(stack, 'Ipam'); const pool = ipam.publicScope.addPool({ addressFamily: AddressFamily.IP_V4, provisionedCidrs: [{ cidr: '10.2.0.0/16' }], + region: 'us-east-1', }); const vpc = new vpc_v2.VpcV2(stack, 'VPCTest', { @@ -36,7 +37,7 @@ const vpc = new vpc_v2.VpcV2(stack, 'VPCTest', { ipv4IpamPoolId: pool.attrIpamPoolId, ipv4NetmaskLength: 20, }), - //vpc_v2.IpAddresses.amazonProvidedIpv6(), + vpc_v2.IpAddresses.amazonProvidedIpv6(), ], enableDnsHostnames: true, enableDnsSupport: true, @@ -45,9 +46,13 @@ const vpc = new vpc_v2.VpcV2(stack, 'VPCTest', { const subnet = new SubnetV2(stack, 'subnet', { vpc, availabilityZone: 'us-west-2a', - cidrBlock: new Ipv4Cidr('10.0.0.0/24'), + cidrBlock: new Ipv6Cidr('10.0.0.0/24'), }); +/** + * Expected as should be true by default + */ + if (!vpc.isolatedSubnets.includes(subnet)) { throw new Error('Subnet is not isolated'); };