import * as aws from "@pulumi/aws" ; import * as pulumi from "@pulumi/pulumi" ; import * as x from ".." ; import { Cidr32Block , getIPv4Address } from "./cidr" ; import * as utils from "../utils" ; internal export interface AvailabilityZoneDescription { name : string ; id : string ; } export function create ( resource , vpcName , vpcCidr , ipv6CidrBlock , availabilityZones , numberOfNatGateways , assignGeneratedIpv6CidrBlock , subnetArgsArray ) { const firstSubnet = subnetArgsArray [ 0 ] ; const hasExplicitLocation = ! ! firstSubnet . location ; for ( const subnet of subnetArgsArray ) { const siblingHasLocation = ! ! subnet . location ; if ( hasExplicitLocation !== siblingHasLocation ) { throw new pulumi . ResourceError ( "[location] property must be specified for either no subnets or all of the subnets." , resource ) ; } if ( siblingHasLocation && subnet . cidrMask !== undefined ) { throw new pulumi . ResourceError ( "Subnet cannot specify [location] and [cidrMask]" , resource ) ; } } const topology = hasExplicitLocation ? new ExplicitLocationTopology ( resource , vpcName , vpcCidr , ipv6CidrBlock , availabilityZones , numberOfNatGateways , assignGeneratedIpv6CidrBlock ) : new ComputedLocationTopology ( resource , vpcName , vpcCidr , ipv6CidrBlock , availabilityZones , numberOfNatGateways , assignGeneratedIpv6CidrBlock ) ; return topology . create ( subnetArgsArray ) ; } internal abstract class VpcTopology { protected readonly vpcCidrBlock : Cidr32Block ; protected lastAllocatedSubnetCidrBlock ? : Cidr32Block ; constructor ( protected readonly resource , protected readonly vpcName , vpcCidr , protected readonly ipv6CidrBlock , protected readonly availabilityZones , protected readonly numberOfNatGateways , protected readonly assignGeneratedIpv6CidrBlock ) { this . vpcCidrBlock = Cidr32Block . fromCidrNotation ( vpcCidr ) ; } public abstract create ( subnetArgsArray ) ; protected shouldCreateNatGateways ( publicSubnets , privateSubnets ) { return this . numberOfNatGateways > 0 && publicSubnets . length > 0 && privateSubnets . length > 0 ; } } class ComputedLocationTopology extends VpcTopology { public create ( subnetArgsArray ) { const maskedSubnets = subnetArgsArray . filter ( s => s . cidrMask !== undefined ) ; const unmaskedSubnets = subnetArgsArray . filter ( s => s . cidrMask === undefined ) ; const subnetDescriptions = [ ] ; const natGateways = [ ] ; const natRoutes = [ ] ; for ( const subnetArgs of maskedSubnets ) { subnetDescriptions . push ( ... this . createSubnetsWorker ( subnetArgs , subnetArgs . cidrMask ! , subnetDescriptions . length ) ) ; } const cidrMaskForUnmaskedSubnets = this . computeCidrMaskForSubnets ( unmaskedSubnets , unmaskedSubnets . length > 0 ) ; for ( const subnetArgs of unmaskedSubnets ) { subnetDescriptions . push ( ... this . createSubnetsWorker ( subnetArgs , cidrMaskForUnmaskedSubnets , subnetDescriptions . length ) ) ; } const publicSubnets = subnetDescriptions . filter ( d => d . type === "public" ) ; const privateSubnets = subnetDescriptions . filter ( d => d . type === "private" ) ; if ( this . shouldCreateNatGateways ( publicSubnets , privateSubnets ) ) { const numberOfAvailabilityZones = this . availabilityZones . length ; if ( this . numberOfNatGateways > numberOfAvailabilityZones ) { throw new Error ( `[numberOfNatGateways] cannot be greater than [numberOfAvailabilityZones]: ${ this . numberOfNatGateways } > ${ numberOfAvailabilityZones }` ) ; } for ( let i = 0 ; i < this . numberOfNatGateways ; i ++ ) { const publicSubnetIndex = i % numberOfAvailabilityZones ; natGateways . push ( { name : `${ this . vpcName }-${ i }` , publicSubnet : publicSubnets [ publicSubnetIndex ] . subnetName , } ) ; } let roundRobinIndex = 0 ; for ( let i = 0 , n = privateSubnets . length ; i < n ; i += numberOfAvailabilityZones ) { for ( let j = 0 ; j < numberOfAvailabilityZones ; j ++ ) { const privateSubnetIndex = i + j ; const natGatewayIndex = j < this . numberOfNatGateways ? j : ( roundRobinIndex ++ % natGateways . length ) ; natRoutes . push ( { name : `nat-${ j }` , privateSubnet : privateSubnets [ privateSubnetIndex ] . subnetName , natGateway : natGateways [ natGatewayIndex ] . name , } ) ; } } } return { subnets : subnetDescriptions , natGateways , natRoutes } ; } private computeCidrMaskForSubnets ( subnets , checkResult ) { const requiredCidrBlockCount = subnets . length * this . availabilityZones . length ; const firstAvailableIp = this . getNextCidrBlockStartingAddress ( ) ; const availableIps = this . vpcCidrBlock . endIpAddressExclusive - firstAvailableIp ; const ipsPerBlock = Math . floor ( availableIps / requiredCidrBlockCount ) ; const result = 32 - Math . floor ( Math . log2 ( ipsPerBlock ) ) ; if ( checkResult ) { if ( result > 28 ) { throw new Error ( `Not enough address space in VPC to create desired subnet config.
VPC has ${ availableIps } IPs, but is being asked to split into a total of ${ requiredCidrBlockCount } subnets.
${ requiredCidrBlockCount } subnets are necessary to have ${ this . availabilityZones . length } AZ(s) each with ${ subnets . length } subnet(s) in them.
This needs ${ ipsPerBlock } IPs/subnet, which is smaller than the minimum (16) allowed by AWS.` ) ; } } return result ; } private getNextCidrBlockStartingAddress ( ) { return ! this . lastAllocatedSubnetCidrBlock ? this . vpcCidrBlock . startIpAddressInclusive : this . lastAllocatedSubnetCidrBlock . endIpAddressExclusive ; } private assignNextAvailableCidrBlock ( mask ) { const nextStartIpAddressInclusive = this . getNextCidrBlockStartingAddress ( ) ; const nextCidrBlock = new Cidr32Block ( nextStartIpAddressInclusive , mask ) ; if ( nextCidrBlock . endIpAddressExclusive > this . vpcCidrBlock . endIpAddressExclusive ) { const lastAllocatedIpAddress = getIPv4Address ( nextCidrBlock . endIpAddressExclusive ) ; const lastVpcIpAddress = getIPv4Address ( this . vpcCidrBlock . endIpAddressExclusive ) ; throw new Error ( `Subnet cidr block end ip address extends past that last legal ip address for the vpc.
${ lastAllocatedIpAddress } > ${ lastVpcIpAddress }` ) ; } this . lastAllocatedSubnetCidrBlock = nextCidrBlock ; return nextCidrBlock ; } private createSubnetsWorker ( subnetArgs , cidrMask , currentSubnetIndex ) { if ( cidrMask < 16 || cidrMask > 28 ) { throw new Error ( `Cidr mask must be between "16" and "28" but was ${ cidrMask }` ) ; } const result = [ ] ; const type = subnetArgs . type ; for ( let i = 0 ; i < this . availabilityZones . length ; i ++ ) { const subnetName = getSubnetName ( this . vpcName , subnetArgs , i ) ; const assignIpv6AddressOnCreation = utils . ifUndefined ( subnetArgs . assignIpv6AddressOnCreation , this . assignGeneratedIpv6CidrBlock ) ; const ipv6CidrBlock = this . createIpv6CidrBlock ( assignIpv6AddressOnCreation , currentSubnetIndex ++ ) ; result . push ( { type , subnetName , args : { availabilityZone : this . availabilityZones [ i ] . name , availabilityZoneId : this . availabilityZones [ i ] . id , cidrBlock : this . assignNextAvailableCidrBlock ( cidrMask ) . toString ( ) , ipv6CidrBlock , mapPublicIpOnLaunch : utils . ifUndefined ( subnetArgs . mapPublicIpOnLaunch , type === "public" ) , assignIpv6AddressOnCreation , tags : subnetArgs . tags , } , ignoreChanges : subnetArgs . ignoreChanges , } ) ; } return result ; function getSubnetName ( vpcName , subnetArgs , i ) { let subnetName = `${ subnetArgs . type }-${ i }` ; if ( subnetArgs . name ) { subnetName = `${ subnetArgs . name }-` + subnetName ; } return `${ vpcName }-${ subnetName }` ; } } private createIpv6CidrBlock ( assignIpv6AddressOnCreation , index ) { const result = pulumi . all ( [ this . ipv6CidrBlock , assignIpv6AddressOnCreation ] ) . apply ( ( [ vpcIpv6CidrBlock , assignIpv6AddressOnCreation ] ) => { if ( ! assignIpv6AddressOnCreation ) { return undefined ; } if ( ! vpcIpv6CidrBlock ) { throw new pulumi . ResourceError ( "Must set [assignGeneratedIpv6CidrBlock] to true on [Vpc] in order to assign ipv6 address to subnet." , this . resource ) ; } const colonColonIndex = vpcIpv6CidrBlock . indexOf ( "::" ) ; if ( colonColonIndex < 0 || vpcIpv6CidrBlock . substr ( colonColonIndex ) !== "::/56" ) { throw new pulumi . ResourceError ( `Vpc ipv6 cidr block was not in an expected form: ${ vpcIpv6CidrBlock }` , this . resource ) ; } const header = vpcIpv6CidrBlock . substr ( 0 , colonColonIndex ) ; if ( ! header . endsWith ( "00" ) ) { throw new pulumi . ResourceError ( `Vpc ipv6 cidr block was not in an expected form: ${ vpcIpv6CidrBlock }` , this . resource ) ; } const prefix = header . substr ( 0 , header . length - 2 ) ; return prefix + index . toString ( ) . padStart ( 2 , "0" ) + "::/64" ; } ) ; return < pulumi . Output < string > > result ; } } class ExplicitLocationTopology extends VpcTopology { public create ( subnets ) { const subnetDescriptions = [ ] ; const natGateways = [ ] ; const natRoutes = [ ] ; if ( subnets . length > 0 ) { type AZ = string | undefined ; const azToPublicSubnets = new Map < AZ , SubnetDescription [ ] > ( ) ; const azToPrivateSubnets = new Map < AZ , SubnetDescription [ ] > ( ) ; for ( let i = 0 , n = subnets . length ; i < n ; i ++ ) { const subnetArgs = subnets [ i ] ; const location = typeof subnetArgs . location === "string" ? { cidrBlock : subnetArgs . location } : subnetArgs . location ! ; const type = subnetArgs . type ; const subnetName = subnetArgs . name || `${ type }-${ i }` ; const subnetDesc = { subnetName , type , args : { ... location , mapPublicIpOnLaunch : utils . ifUndefined ( subnetArgs . mapPublicIpOnLaunch , type === "public" ) , assignIpv6AddressOnCreation : utils . ifUndefined ( subnetArgs . assignIpv6AddressOnCreation , this . assignGeneratedIpv6CidrBlock ) , tags : subnetArgs . tags , } , ignoreChanges : subnetArgs . ignoreChanges , } ; subnetDescriptions . push ( subnetDesc ) ; const az = location . availabilityZone || location . availabilityZoneId ; const specificSubnetMap = type === "public" ? azToPublicSubnets : type === "private" ? azToPrivateSubnets : undefined ; if ( specificSubnetMap ) { const specificSubnets = specificSubnetMap . get ( az ) || [ ] ; specificSubnets . push ( subnetDesc ) ; specificSubnetMap . set ( az , specificSubnets ) ; } } const publicSubnets = subnetDescriptions . filter ( d => d . type === "public" ) ; const privateSubnets = subnetDescriptions . filter ( d => d . type === "private" ) ; if ( this . shouldCreateNatGateways ( publicSubnets , privateSubnets ) ) { const azToNatGateway = new Map < string | undefined , NatGatewayDescription > ( ) ; const privateSubnetAzs = [ ... azToPrivateSubnets . keys ( ) ] . sort ( ) ; const minNatGateways = Math . min ( privateSubnetAzs . length , this . numberOfNatGateways ) ; const publicSubnetsWithNatGateway = new Set < SubnetDescription > ( ) ; for ( let i = 0 ; i < minNatGateways ; i ++ ) { const az = privateSubnetAzs [ i ] ; let publicSubnetForAz ; if ( azToPublicSubnets . has ( az ) ) { publicSubnetForAz = azToPublicSubnets . get ( az ) ! [ 0 ] ; } else { for ( const [ _ , publicSubnetsForOtherAz ] of azToPublicSubnets ) { if ( ! publicSubnetsWithNatGateway . has ( publicSubnetsForOtherAz [ 0 ] ) ) { publicSubnetForAz = publicSubnetsForOtherAz [ 0 ] ; break ; } } } if ( ! publicSubnetForAz ) { continue ; } publicSubnetsWithNatGateway . add ( publicSubnetForAz ) ; const natGateway = { name : `${ this . vpcName }-${ i }` , publicSubnet : publicSubnetForAz . subnetName } ; azToNatGateway . set ( az , natGateway ) ; natGateways . push ( natGateway ) ; } let natGatewayRoundRobinIndex = 0 ; let routeIndex = 0 ; for ( const az of privateSubnetAzs ) { const privateSubnetsInAz = azToPrivateSubnets . get ( az ) ! ; const natGatewayInAz = azToNatGateway . get ( az ) ; for ( let i = 0 , n = privateSubnetsInAz . length ; i < n ; i ++ ) { const privateSubnet = privateSubnetsInAz [ i ] ; const natGateway = natGatewayInAz ? natGatewayInAz : natGateways [ natGatewayRoundRobinIndex ++ % natGateways . length ] ; natRoutes . push ( { name : `nat-${ routeIndex ++ }` , privateSubnet : privateSubnet . subnetName , natGateway : natGateway . name , } ) ; } } } } return { subnets : subnetDescriptions , natGateways , natRoutes } ; } } internal export interface VpcTopologyDescription { subnets : SubnetDescription [ ] ; natGateways : NatGatewayDescription [ ] ; natRoutes : NatRouteDescription [ ] ; } internal export interface SubnetDescription { type : x . ec2 . VpcSubnetType ; subnetName : string ; args : x . ec2 . SubnetArgs ; ignoreChanges ? : string [ ] ; } internal export interface NatGatewayDescription { name : string ; /** index of the public subnet that this nat gateway should live in. */ publicSubnet : string ; } internal export interface NatRouteDescription { name : string ; /** The name of the private subnet that is getting the route */ privateSubnet : string ; /** The name of the nat gateway this private subnet is getting a route to. */ natGateway : string ; }