diff --git a/bin/vpc-builder.ts b/bin/vpc-builder.ts index cda44e9..0e581d4 100644 --- a/bin/vpc-builder.ts +++ b/bin/vpc-builder.ts @@ -5,7 +5,7 @@ import { Stack } from "aws-cdk-lib" (async () => { try { - const stackBuilder = new StackBuilderClass(); + const stackBuilder = new StackBuilderClass({}); const cdkApp = stackBuilder.stackMapper.app; const configFile = cdkApp.node.tryGetContext("config"); @@ -16,7 +16,7 @@ import { Stack } from "aws-cdk-lib" } else { // When no configuration context provided, we will warn but not fail. This allows 'cdk bootstrap', 'cdk help' // to continue to work as expected. - const dummyStack = new Stack(cdkApp, 'dummyStack', {}) + new Stack(cdkApp, 'dummyStack', {}) console.warn( "\nNo configuration provided. Use a configuration file from the 'config' directory using the '-c config=[filename]' argument\n" ); diff --git a/config/config-walkthrough.yaml b/config/config-walkthrough.yaml index b7086b7..03039b2 100644 --- a/config/config-walkthrough.yaml +++ b/config/config-walkthrough.yaml @@ -248,6 +248,23 @@ vpns: # it must exist with the same name in the transitGateways section. useTransit: central +# OPTIONAL: These define an EXISTING Direct Connect Gateway attached to an EXISTING Transit Gateway +# The Transit Gateway must be the same one you define under `transitGateways`. +# When creating the Transit Gateway using vpcBuilder: +# 1) Deploy the transit Gateway without modelling the `dxgws:`. +# 2) Attach Direct Connect Gateway to the Transit Gateway deployed in 1) +# 3) Create a Transit Gateway Route Table and associate it with the Direct Connect Gateway +# 4) Model the `dxgws:` section below with the Transit Gateway ID, Route Table ID, and Attachment ID from above +# 5) Refer to the dxgw in `routesTo:` sections in you Transit Gateway Below +dxgws: + toGroundDataCentersDx: + # REQUIRED: Existing Transit Gateway (see above if you're relying on vpcBuilder to create the TGW) + existingTgwId: tgw-12345 + # REQUIRED: Existing Transit Gateway Attachment Identifier for the Direct Connect Gateway + existingDxGwTransitGatewayAttachId: tgw-attach-12345 + # REQUIRED: Existing Transit Gateway Route Table associated with the Direct Connect Gateway + existingDxGwTransitGatewayRouteTableId: tgw-rtb-12345 + # OPTIONAL|DEPENDANT. If any VPCs refer to, providers are defined, or VPNs are defined, this section is required. # this describes the transit gateways to create (NOTE: at-present we only support one per config file). transitGateways: @@ -264,7 +281,7 @@ transitGateways: defaultRoutes: # REQUIRED: The name of the thing we're routing from. This can be a VPC, or a Provider name. - vpcName: workload - # REQUIRED: The name of the thing we're routing toward. This can be a VPC, VPN, or Provider name. + # REQUIRED: The name of the thing we're routing toward. This can be a VPC, VPN, DxGw, or Provider name. routesTo: egressViaNat # OPTIONAL: If we're going to inspect the traffic. This must be the name of a firewall provider. Routes are adjusted automatically # to assure it passes through the inspection VPC before arriving at its routesTo and vice versa. @@ -277,7 +294,7 @@ transitGateways: dynamicRoutes: # REQUIRED: The name of the thing we're routing from. This can be a VPC, or a Provider name. - vpcName: workload - # REQUIRED: The name of the thing we're routing toward. This can be a VPC, VPN, or Provider name. + # REQUIRED: The name of the thing we're routing toward. This can be a VPC, VPN, DxGw, or Provider name. routesTo: workloadTwo # OPTIONAL: The name of the firewall provider to inspect traffic. Routes adjust automatically. # NOTE: inspectedBy is not available for VPN connections with a dynamic route. Use Static, or Default route instead. @@ -288,7 +305,7 @@ transitGateways: staticRoutes: # REQUIRED: The name of the thing we're routing from. This can be a VPC, or a Provider name. - vpcName: workload - # REQUIRED: The name of the thing we're routing toward. This can be a VPC, VPN, or Provider name. + # REQUIRED: The name of the thing we're routing toward. This can be a VPC, VPN, DxGw or Provider name. routesTo: toGroundDataCenterOne # REQUIRED: the CIDR address for the static route entry staticCidr: 192.168.168.0/24 diff --git a/lambda/findVpnTransitGatewayAttachId/index.ts b/lambda/findVpnTransitGatewayAttachId/index.ts index cc27298..08b9890 100644 --- a/lambda/findVpnTransitGatewayAttachId/index.ts +++ b/lambda/findVpnTransitGatewayAttachId/index.ts @@ -48,9 +48,8 @@ export const onEvent = async (event: CdkCustomResourceEvent) => { }; if (event.RequestType == "Create" || event.RequestType == "Update") { - const transitGatewayAttachId = await findVpnTransitGatewayAttachId( - requestProps, - ); + const transitGatewayAttachId = + await findVpnTransitGatewayAttachId(requestProps); console.info(`Retrieved identifier: ${transitGatewayAttachId}`); responseProps.Data = { transitGatewayAttachId: transitGatewayAttachId, diff --git a/lib/abstract-builderdxgw.ts b/lib/abstract-builderdxgw.ts new file mode 100644 index 0000000..50cd2dc --- /dev/null +++ b/lib/abstract-builderdxgw.ts @@ -0,0 +1,73 @@ +import * as cdk from "aws-cdk-lib"; +import { Construct } from "constructs"; +import { + IBuilderTgwStaticRoutes, + IBuilderDxGw, + IBuilderDxGwProps, + ITgwAttachType, + ITgwPropagateRouteAttachmentName, + ssmParameterImport, + ITgw, + ITgwRouteTable, + ITgwAttachment, +} from "./types"; +import * as ssm from "aws-cdk-lib/aws-ssm"; + +export abstract class BuilderDxGw extends cdk.Stack implements IBuilderDxGw { + name: string; + globalPrefix: string; + // Always attached to a Transit Gateway + withTgw: true; + // Always false since this isn't VPC Based + tgwCreateTgwSubnets: false; + tgwAttachType: ITgwAttachType = "dxgw" + tgw: ITgw; + tgwRouteTable: ITgwRouteTable; + tgwRouteTableSsm: ssmParameterImport; + tgwAttachment: ITgwAttachment; + tgwAttachmentSsm: ssmParameterImport; + tgwPropagateRouteAttachmentNames: Array = + []; + // Blackhole CIDRs not applicable for an imported DxGw + readonly tgwBlackHoleCidrs: []; + tgwStaticRoutes: Array = []; + tgwDefaultRouteAttachmentName: ITgwPropagateRouteAttachmentName; + props: IBuilderDxGwProps; + + protected constructor(scope: Construct, id: string, props: IBuilderDxGwProps) { + super(scope, id, props); + this.props = props; + this.globalPrefix = props.globalPrefix.toLowerCase(); + } + + // We only support imports, but this method is common to all stacks so needs to be present + saveTgwRouteInformation() { + } + + async init() {} + + createSsmParameters() { + const prefix = + `${this.props.ssmParameterPrefix}/networking/${this.globalPrefix}/dxgw/${this.name}`.toLowerCase(); + + this.tgwRouteTableSsm = { + name: `${prefix}/tgwRouteId`, + }; + new ssm.StringParameter(this, `ssmDxGwTgwRouteTableSsm`, { + parameterName: `${prefix}/tgwRouteId`, + stringValue: this.tgwRouteTable.ref, + }); + + this.tgwAttachmentSsm = { + name: `${prefix}/tgwAttachId`, + }; + new ssm.StringParameter(this, `ssmDxGwTgwAttachIdSsm`, { + parameterName: `${prefix}/tgwAttachId`, + stringValue: this.tgwAttachment.attrId, + }); + } + + // We only support imports, but this method is common to all stacks so needs to be present + attachToTGW() { + } +} diff --git a/lib/cdk-export-presistence-stack.ts b/lib/cdk-export-presistence-stack.ts index 05a6ebf..c6cfbb3 100644 --- a/lib/cdk-export-presistence-stack.ts +++ b/lib/cdk-export-presistence-stack.ts @@ -1,10 +1,10 @@ import * as cdk from "aws-cdk-lib"; import { Construct } from "constructs"; import { BuilderVpc } from "./abstract-buildervpc"; -import { IBuilderVpc, IBuilderVpn } from "./types"; +import {IBuilderDxGw, IBuilderVpc, IBuilderVpn} from "./types"; export interface ICdkExportPersistenceProps extends cdk.StackProps { - persistExports: Array; + persistExports: Array; } export class CdkExportPersistenceStack extends cdk.Stack { diff --git a/lib/config/config-schema.json b/lib/config/config-schema.json index bd6af0d..9c5f6df 100644 --- a/lib/config/config-schema.json +++ b/lib/config/config-schema.json @@ -57,6 +57,32 @@ ], "type": "object" }, + "IConfigDxGw": { + "additionalProperties": false, + "properties": { + "existingDxGwTransitGatewayAttachId": { + "type": "string" + }, + "existingDxGwTransitGatewayRouteTableId": { + "type": "string" + }, + "existingTgwId": { + "type": "string" + } + }, + "required": [ + "existingDxGwTransitGatewayAttachId", + "existingDxGwTransitGatewayRouteTableId", + "existingTgwId" + ], + "type": "object" + }, + "IConfigDxGws": { + "additionalProperties": { + "$ref": "#/definitions/IConfigDxGw" + }, + "type": "object" + }, "IConfigGlobal": { "additionalProperties": false, "properties": { @@ -576,6 +602,9 @@ "dns": { "$ref": "#/definitions/IConfigDns" }, + "dxgws": { + "$ref": "#/definitions/IConfigDxGws" + }, "global": { "$ref": "#/definitions/IConfigGlobal" }, diff --git a/lib/config/config-types.ts b/lib/config/config-types.ts index ba448fe..ea5fe17 100644 --- a/lib/config/config-types.ts +++ b/lib/config/config-types.ts @@ -113,6 +113,20 @@ export interface IConfigVpns { [key: string]: IConfigVpn; } +/* + ****** Direct Connect Gateways: + */ + +export interface IConfigDxGw { + existingTgwId: string; + existingDxGwTransitGatewayAttachId: string; + existingDxGwTransitGatewayRouteTableId: string; +} + +export interface IConfigDxGws { + [key: string]: IConfigDxGw; +} + /* ****** dns: */ @@ -215,6 +229,7 @@ export interface IConfig { providers?: IConfigProviders; vpcs: IConfigVpcs; vpns?: IConfigVpns; + dxgws?: IConfigDxGws; dns?: IConfigDns; transitGateways?: IConfigTgws; } diff --git a/lib/config/parser.ts b/lib/config/parser.ts index 72cc3d7..3786934 100644 --- a/lib/config/parser.ts +++ b/lib/config/parser.ts @@ -4,6 +4,7 @@ import * as configSchema from "./config-schema.json"; import * as fs from "fs"; import * as path from "path"; import * as yaml from "yaml"; + const IPCidr = require("ip-cidr"); const avj = new Ajv({ allowUnionTypes: true }); @@ -64,11 +65,12 @@ export class ConfigParser { // ** Global Section Verifications. this.verifySsmPrefix(); this.verifyDiscoveryFolder(); + // Confirm unique naming within the config file for all resources + this.verifyResourceNamesUnique(); // ** Providers if (configRaw.hasOwnProperty("providers")) { this.verifyProvidersTransitsExist(); - this.verifyProviderVpnAndVpcNamesUnique(); this.verifyVpcCidrsProviders(); this.verifyProviderEndpoints(); this.verifyInternetProviderRoutes(); @@ -85,11 +87,16 @@ export class ConfigParser { this.verifyVpnsTransitExists(); } + //** DxGateways + if (configRaw.hasOwnProperty("dxgws")) { + this.dxGwAssureRequiredArguments(); + } + // ** Transits if (configRaw.hasOwnProperty("transitGateways")) { this.verifyOnlyOneTransitGateway(); this.verifyTransitGatewayOptions(); - this.tgwRoutesHaveVpnsVpcsOrProviders(); + this.tgwRouteChecks(); this.verifyCidrsTransitGateway(); } @@ -100,7 +107,6 @@ export class ConfigParser { this.verifyVpcWithNoTransitHasNoRoutes(); // Our schema matches! Lets load it up for further value verification. this.config = this.configRaw as any; - // TODO verification of content } verifyVpcCidrsProviders() { @@ -428,6 +434,22 @@ export class ConfigParser { return false; } + allProviderNames(): Array { + const providerNames: Array = []; + for (const providerType of ["endpoints", "internet", "firewall"]) { + if (this.configRaw.hasOwnProperty("providers")) { + if (this.configRaw.providers.hasOwnProperty(providerType)) { + for (const providerName of Object.keys( + this.configRaw.providers[providerType], + )) { + providerNames.push(providerName); + } + } + } + } + return providerNames; + } + vpcNameExists(checkVpcName: string) { for (const vpcName of Object.keys(this.configRaw.vpcs)) { if (vpcName == checkVpcName) { @@ -437,10 +459,28 @@ export class ConfigParser { return false; } + // Update as needed as more resources are supported + allResourceNames(): Array { + return [ + ...this.allVpcNames(), + ...this.allVpnNames(), + ...this.allProviderNames(), + ...this.allDxGwNames(), + ]; + } + + allVpcNames(): Array { + const vpcNames: Array = []; + for (const vpcName of Object.keys(this.configRaw.vpcs)) { + vpcNames.push(vpcName); + } + return vpcNames; + } + vpnNameExists(checkVpnName: string) { if (this.configRaw.vpns) { - for (const vpcName of Object.keys(this.configRaw.vpns)) { - if (vpcName == checkVpnName) { + for (const vpnName of Object.keys(this.configRaw.vpns)) { + if (vpnName == checkVpnName) { return true; } } @@ -448,28 +488,51 @@ export class ConfigParser { return false; } - verifyProviderVpnAndVpcNamesUnique() { - for (const vpcName of Object.keys(this.configRaw.vpcs)) { - if (this.providerNameExists(vpcName)) { - throw new Error( - `Name Providers, VPNs and Vpcs with unique names. Duplicate name ${vpcName} was found`, - ); - } - if (this.vpnNameExists(vpcName)) { - throw new Error( - `Name Providers, VPNs and Vpcs with unique names. Duplicate name ${vpcName} was found`, - ); + allVpnNames(): Array { + const vpnNames: Array = []; + if (this.configRaw.vpns) { + for (const vpnName of Object.keys(this.configRaw.vpns)) { + vpnNames.push(vpnName); } - if (this.configRaw.hasOwnProperty("vpns")) { - for (const vpnName of Object.keys(this.configRaw.vpns)) { - if (this.providerNameExists(vpcName)) { - throw new Error( - `Name Providers, VPNs and Vpcs with unique names. Duplicate name ${vpcName} was found`, - ); - } + } + return vpnNames; + } + + dxgwNameExists(checkDxGwName: string) { + if (this.configRaw.dxgws) { + for (const dxgwName of Object.keys(this.configRaw.dxgws)) { + if (dxgwName == checkDxGwName) { + return true; } } } + return false; + } + + allDxGwNames(): Array { + const dxGwNames: Array = []; + if (this.configRaw.dxgws) { + for (const dxgwName of Object.keys(this.configRaw.dxgws)) { + dxGwNames.push(dxgwName); + } + } + return dxGwNames; + } + + verifyResourceNamesUnique() { + // Find all our names in our config file. + const allNames = this.allResourceNames(); + // Our resulting array should not have any duplicate members + const countOccurrences = (arr: Array, val: string) => + arr.reduce((a, v) => (v === val ? a + 1 : a), 0); + const uniqueList = new Set(allNames); + uniqueList.forEach((uniqueName) => { + if (countOccurrences(allNames, uniqueName) > 1) { + throw new Error( + `Providers, VPNs, VPCs, and DxGws must be named uniquely within the config file. Duplicate name ${uniqueName} was found`, + ); + } + }); } // VPN can be imported, with existing customer gateway or without. @@ -557,6 +620,36 @@ export class ConfigParser { } } + // Direct Connect Gateway is always imported. Assure expected format exists for our values. + dxGwAssureRequiredArguments() { + for (const dxGwName of Object.keys(this.configRaw.dxgws)) { + const configStanza = this.configRaw.dxgws[dxGwName]; + if (!configStanza.existingTgwId.startsWith("tgw-")) { + throw new Error( + `DxGw: ${dxGwName}: Existing Transit Gateway 'existingTgwId' must begin with tgw-`, + ); + } + if ( + !configStanza.existingDxGwTransitGatewayAttachId.startsWith( + "tgw-attach-", + ) + ) { + throw new Error( + `DxGw: ${dxGwName}: Transit Gateway Attachment Value 'existingDxGwTransitGatewayAttachId' must begin with tgw-attach-`, + ); + } + if ( + !configStanza.existingDxGwTransitGatewayRouteTableId.startsWith( + "tgw-rtb-", + ) + ) { + throw new Error( + `DxGw: ${dxGwName}: Transit Gateway Route Table Value 'existingDxGwTransitGatewayRouteTableId' must begin with tgw-rtb-`, + ); + } + } + } + dnsVerifyRequiredArguments() { for (const dnsConfigName of Object.keys(this.configRaw.dns)) { const configStanza = this.configRaw.dns[dnsConfigName]; @@ -606,144 +699,215 @@ export class ConfigParser { } } - tgwRoutesHaveVpnsVpcsOrProviders() { - if (this.configRaw.hasOwnProperty("transitGateways")) { - for (const transitGatewayName of Object.keys( - this.configRaw.transitGateways, - )) { - const configStanza = this.configRaw.transitGateways[transitGatewayName]; - if (configStanza.blackholeRoutes) { - for (const route of configStanza.blackholeRoutes) { - if ( - !this.vpcNameExists(route.vpcName) && - !this.providerNameExists(route.vpcName) && - !this.vpnNameExists(route.vpcName) - ) { + // route vpcName points to a vpc + // route routesTo points to a valid resource in the config file + twgRouteNamesValid(configStanza: any) { + const routeTypes = [ + "blackholeRoutes", + "staticRoutes", + "dynamicRoutes", + "defaultRoutes", + ]; + const routeHuman: Record = { + blackholeRoutes: "blackhole route", + staticRoutes: "static route", + dynamicRoutes: "dynamic route", + defaultRoutes: "default route", + }; + const allNames = this.allResourceNames(); + + routeTypes.forEach((routeType) => { + if (configStanza[routeType]) { + for (const route of configStanza[routeType]) { + // vpcName points to a vpc + if (!this.vpcNameExists(route.vpcName)) { + if (allNames.includes(route.vpcName)) { + // If vpcName points to a non-vpc provide a more useful message throw new Error( - `A blackhole route was specified for ${route.vpcName} but no vpc, vpn or provider with that name could be found`, + `Invalid vpcName specified for ${routeHuman[routeType]}. 'vpcName: ${route.vpcName}'. A non-VPC resource is using this name.`, ); - } - if (this.vpnNameExists(route.vpcName)) { + } else { throw new Error( - `A blackhole route was specified with ${route.vpcName}. This is a VPN and cannot be in the 'vpcName' field. You can only 'routeTo' VPNs`, + `A ${routeHuman[routeType]} was specified for ${route.vpcName} - vpc with that name could not be found`, ); } } - } - if (configStanza.staticRoutes) { - for (const route of configStanza.staticRoutes) { - if ( - !this.vpcNameExists(route.vpcName) && - !this.providerNameExists(route.vpcName) && - !this.vpnNameExists(route.vpcName) - ) { - throw new Error( - `A static route was specified for ${route.vpcName} but no vpc, vpn or provider with that name could be found`, - ); - } - if ( - !this.vpcNameExists(route.routesTo) && - !this.providerNameExists(route.routesTo) && - !this.vpnNameExists(route.routesTo) - ) { - throw new Error( - `A static route was specified for ${route.routesTo} but no vpc, vpn or provider with that name could be found`, - ); - } - if (this.vpnNameExists(route.vpcName)) { + // routesTo points to a valid resource in the config. Not applicable for BlackholeRoutes + if (routeType != "blackholeRoutes") { + if (!allNames.includes(route.routesTo)) { throw new Error( - `A static route was specified with ${route.vpcName}. This is a VPN and cannot be in the 'vpcName' field. You can only 'routeTo' VPNs`, + `A ${routeHuman[routeType]} for VPC Named ${route.vpcName} to route to ${route.routesTo}. Configuration file does not contain a resource named ${route.routesTo}`, ); } - if (route.inspectedBy) { - if (!this.providerNameExists(route.inspectedBy, true)) { - throw new Error( - `A static route is set to be inspected by ${route.inspectedBy} but no firewall provider with that name was found`, - ); - } - } } } - if (configStanza.dynamicRoutes) { - for (const route of configStanza.dynamicRoutes) { - if ( - !this.vpcNameExists(route.vpcName) && - !this.providerNameExists(route.vpcName) && - !this.vpnNameExists(route.vpcName) - ) { - throw new Error( - `A dynamic route was specified for ${route.vpcName} but no vpc, vpn or provider with that name could be found`, - ); - } - if ( - !this.vpcNameExists(route.routesTo) && - !this.providerNameExists(route.routesTo) && - !this.vpnNameExists(route.routesTo) - ) { - throw new Error( - `A dynamic route was specified for ${route.routesTo} but no vpc, vpn or provider with that name could be found`, - ); - } - if (this.vpnNameExists(route.vpcName)) { - throw new Error( - `A dynamic route was specified with ${route.vpcName}. This is a VPN and cannot be in the 'vpcName' field. You can only 'routeTo' VPNs`, - ); - } - if (route.inspectedBy) { - if (!this.providerNameExists(route.inspectedBy, true)) { - throw new Error( - `A dynamic route is set to be inspected by ${route.inspectedBy} but no firewall provider with that name was found`, - ); - } - if ( - this.vpnNameExists(route.vpcName) || - this.vpnNameExists(route.routesTo) - ) { - throw new Error( - `VPN inspection is not possible via Dynamic Routing. Implement via Static or Default Route instead.`, - ); - } - } - } - } - if (configStanza.defaultRoutes) { - for (const route of configStanza.defaultRoutes) { - if ( - !this.vpcNameExists(route.vpcName) && - !this.providerNameExists(route.vpcName) && - !this.vpnNameExists(route.vpcName) - ) { - throw new Error( - `A default route was specified for ${route.vpcName} but no vpc, vpn or provider with that name could be found`, - ); - } - if ( - !this.vpcNameExists(route.routesTo) && - !this.providerNameExists(route.routesTo) && - !this.vpnNameExists(route.routesTo) - ) { - throw new Error( - `A default route was specified for ${route.routesTo} but no vpc, vpn or provider with that name could be found`, - ); - } - if (this.vpnNameExists(route.vpcName)) { + } + }); + } + + // Where present, inspectedBy routes a valid + // Dynamic route inspectedBy where routesTo is a VPN is not supported + twgRouteInspectedByValid(configStanza: any) { + const routeTypes = ["staticRoutes", "dynamicRoutes", "defaultRoutes"]; + const routeHuman: Record = { + staticRoutes: "static route", + dynamicRoutes: "dynamic route", + defaultRoutes: "default route", + }; + + routeTypes.forEach((routeType) => { + if (configStanza[routeType]) { + for (const route of configStanza[routeType]) { + if (route.inspectedBy) { + if (!this.providerNameExists(route.inspectedBy, true)) { throw new Error( - `A default route was specified with ${route.vpcName}. This is a VPN and cannot be in the 'vpcName' field. You can only 'routeTo' VPNs`, + `A ${routeHuman[routeType]} is set to be inspected by ${route.inspectedBy} but no firewall provider with that name was found`, ); } - if (route.inspectedBy) { - if (!this.providerNameExists(route.inspectedBy, true)) { + // Dynamic routes where routeTo is a VPN are not supported + if (routeType == "dynamicRoutes") { + if (this.vpnNameExists(route.routesTo)) { throw new Error( - `A default route is set to be inspected by ${route.inspectedBy} but no firewall provider with that name was found`, + `VPN as the 'routesTo' destination with inspection is not possible using Dynamic Routing. Implement via Static or Default Route instead.`, ); } } } } } + }); + } + + tgwRouteChecks() { + if (this.configRaw.hasOwnProperty("transitGateways")) { + for (const transitGatewayName of Object.keys( + this.configRaw.transitGateways, + )) { + const configStanza = this.configRaw.transitGateways[transitGatewayName]; + // vpcName is a VPC. routesTo is valid. For all Route Types + this.twgRouteNamesValid(configStanza); + // InspectedBy - if configured for static, dynamic, default points to a valid firewall + this.twgRouteInspectedByValid(configStanza); + } } } + // within the structure of [ vpcName: string, routesTo: string ] only vpcNames may be present + // BlackHole routes may only be specified for vpcs + // Verify that our references are valid for routesTo, inspectedBy + // tgwRoutesAreSane() { + // if (this.configRaw.hasOwnProperty("transitGateways")) { + // for (const transitGatewayName of Object.keys( + // this.configRaw.transitGateways, + // )) { + // const configStanza = this.configRaw.transitGateways[transitGatewayName]; + // const allNames: Array = [ + // ...this.allVpcNames(), + // ...this.allVpnNames(), + // ...this.allProviderNames(), + // ...this.allDxGwNames() + // ] + // // BlackHole routes may only be specified for vpcs + // if (configStanza.blackholeRoutes) { + // for (const route of configStanza.blackholeRoutes) { + // if (!this.vpcNameExists(route.vpcName)) { + // if (allNames.includes(route.vpcName)) { + // throw new Error( + // `Invalid vpcName specified for blackhole route. 'vpcName: ${route.vpcName}'. A non-VPC resource has this name.` + // ) + // } else { + // throw new Error( + // `A blackhole route was specified for ${route.vpcName} - vpc with that name could be found`, + // ); + // } + // } + // } + // } + // // vpcName entry must be a vpc + // // If inspectedBy is specified we must have a firewall definition + // if (configStanza.staticRoutes) { + // for (const route of configStanza.staticRoutes) { + // // Static route must be a vpcName + // if (!this.vpcNameExists(route.vpcName)) { + // if (allNames.includes(route.vpcName)) { + // throw new Error( + // `Invalid vpcName specified for static route. 'vpcName: ${route.vpcName}'. A non-VPC resource has this name.` + // ) + // } else { + // throw new Error( + // `A static route was specified for ${route.vpcName} - vpc with that name could be found`, + // ); + // } + // } + // // We must have a firewall provider by this name + // if (route.inspectedBy) { + // if (!this.providerNameExists(route.inspectedBy, true)) { + // throw new Error( + // `A static route is set to be inspected by ${route.inspectedBy} but no firewall provider with that name was found`, + // ); + // } + // } + // } + // } + // // vpcName entry must be a vpc + // // routesTo must be a valid resource name + // // If inspectedBy is specified we must have a firewall definition + // // routesTo cannot be a VPN if inspectedBy is present + // if (configStanza.dynamicRoutes) { + // for (const route of configStanza.dynamicRoutes) { + // if (!this.vpcNameExists(route.vpcName)) { + // if (allNames.includes(route.vpcName)) { + // throw new Error( + // `Invalid vpcName specified for dynamic route. 'vpcName: ${route.vpcName}'. A non-VPC resource has this name.` + // ) + // } else { + // throw new Error( + // `A dynamic route was specified for ${route.vpcName} - vpc with that name could be found`, + // ); + // } + // } + // if (route.inspectedBy) { + // if (!this.providerNameExists(route.inspectedBy, true)) { + // throw new Error( + // `A dynamic route is set to be inspected by ${route.inspectedBy} but no firewall provider with that name was found`, + // ); + // } + // if (this.vpnNameExists(route.routesTo)) { + // throw new Error( + // `VPN as the 'routesTo' destination with inspection is not possible using Dynamic Routing. Implement via Static or Default Route instead.`, + // ); + // } + // } + // } + // } + // // vpcName entry must be a vpc + // // If inspectedBy is specified we must have a firewall definition + // if (configStanza.defaultRoutes) { + // for (const route of configStanza.defaultRoutes) { + // if (!this.vpcNameExists(route.vpcName)) { + // if (allNames.includes(route.vpcName)) { + // throw new Error( + // `Invalid vpcName specified for default route. 'vpcName: ${route.vpcName}'. A non-VPC resource has this name.` + // ) + // } else { + // throw new Error( + // `A default route was specified for ${route.vpcName} - vpc with that name could be found`, + // ); + // } + // } + // if (route.inspectedBy) { + // if (!this.providerNameExists(route.inspectedBy, true)) { + // throw new Error( + // `A default route is set to be inspected by ${route.inspectedBy} but no firewall provider with that name was found`, + // ); + // } + // } + // } + // } + // } + // } + // } + verifyVpcProvidersExist() { for (const vpcName of Object.keys(this.configRaw.vpcs)) { const vpcConfigStanza = this.configRaw.vpcs[vpcName]; @@ -839,9 +1003,9 @@ export class ConfigParser { return matchingRoutes; } - locateVpcStanzaByName(vpcName: string): any | undefined { + locateVpcStanzaByName(vpcNameFind: string): any | undefined { for (const vpcName of Object.keys(this.configRaw.vpcs)) { - if (vpcName == vpcName) { + if (vpcName == vpcNameFind) { return this.configRaw.vpcs[vpcName]; } } diff --git a/lib/direct-connect-gateway-stack.ts b/lib/direct-connect-gateway-stack.ts new file mode 100644 index 0000000..e2ae22e --- /dev/null +++ b/lib/direct-connect-gateway-stack.ts @@ -0,0 +1,38 @@ +/* + * NOTE: There is no Cloudformation support for Direct Connect at the moment. This will serve as an abstract model + * so we can import the tgw attachments and create static routes and propagations + * See: https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/876 + * Expand in the future to support creation of the Dx Gateway itself when support is added. + */ + +import { Construct } from "constructs"; +import { + IBuilderDxGwProps, +} from "./types"; +import { BuilderDxGw } from "./abstract-builderdxgw"; + +export interface IDirectConnectGatewayProps extends IBuilderDxGwProps { + existingTransitGatewayId: string; + existingDxGwTransitGatewayAttachId: string + existingDxGwTransitGatewayRouteTableId: string +} + +export class DirectConnectGatewayStack extends BuilderDxGw { + props: IDirectConnectGatewayProps; + + constructor(scope: Construct, id: string, props: IDirectConnectGatewayProps) { + super(scope, id, props); + + this.name = `${props.namePrefix}-dxgw`.toLowerCase(); + + this.tgw = { + attrId: this.props.existingTransitGatewayId + } + this.tgwRouteTable = { + ref: this.props.existingDxGwTransitGatewayRouteTableId, + }; + this.tgwAttachment = { + attrId: this.props.existingDxGwTransitGatewayAttachId, + }; + } +} diff --git a/lib/stack-builder.ts b/lib/stack-builder.ts index 43f97e6..754c4a0 100644 --- a/lib/stack-builder.ts +++ b/lib/stack-builder.ts @@ -3,6 +3,7 @@ import "source-map-support/register"; import { ServiceDetail } from "@aws-sdk/client-ec2"; import { IBuilderVpc, + IBuilderDxGw, IVpcWorkloadProps, SubnetNamedMasks, ITgwPropagateRouteAttachmentName, @@ -38,6 +39,11 @@ export interface namedVpnStack { stack: IBuilderVpn; } +export interface namedDxGwStack { + name: string + stack: IBuilderDxGw +} + export type cdkVpcStackTypes = | "providerEndpoint" | "providerInternet" @@ -47,6 +53,7 @@ export type cdkVpcStackTypes = export interface cdkStacks { transitGateway: Array; vpn: Array; + dxgw: Array; providerEndpoint: Array; providerInternet: Array; providerFirewall: Array; @@ -60,16 +67,17 @@ export type providerKeys = export type vpnKeys = "vpn"; export type workloadKeys = "workload"; export type transitGatewayKeys = "transitGateway"; +export type dxGwKeys = "dxgw" export interface IStackBuilderProps {} export class StackBuilderClass { - configFilename: string; - configContents: string; + props: IStackBuilderProps stackMapper: StackMapper; stacks: cdkStacks = { transitGateway: [], vpn: [], + dxgw: [], providerEndpoint: [], providerInternet: [], providerFirewall: [], @@ -80,8 +88,9 @@ export class StackBuilderClass { interfaceDiscovery: Array = []; interfaceList: Array = []; - constructor(props?: IStackBuilderProps) { - this.stackMapper = new StackMapper(); + constructor(props: IStackBuilderProps) { + this.props = props + this.stackMapper = new StackMapper({}); } configure(configFilename?: string, configContents?: string) { @@ -114,6 +123,11 @@ export class StackBuilderClass { await this.buildVpnStacks(); } + // Build our DxGw stack if configured + if(this.c.dxgws) { + await this.buildDxGwStacks(); + } + // Build all of our provider stacks if they are configured if (this.c.providers?.endpoints) { for (let endpointName of Object.keys(this.c.providers?.endpoints)) { @@ -165,7 +179,7 @@ export class StackBuilderClass { // Now our stacks are in place, associate our route relationships between them if (this.c.transitGateways) { this.associateTgwRoutes(); - const allNamedStacks = this.allNamedVpcStacks(); + const allNamedStacks = this.allNamedStacks(); this.stackMapper.transitGatewayRoutesStack("transit-gateway-routes", { tgwAttachmentsAndRoutes: allNamedStacks, useLegacyIdentifiers: this.c.global.useLegacyIdentifiers ? this.c.global.useLegacyIdentifiers : false @@ -330,6 +344,29 @@ export class StackBuilderClass { } } + async buildDxGwStacks() { + for (const dxGwName of Object.keys(this.c.dxgws!)) { + const configStanza = this.c.dxgws![dxGwName]; + this.stacks.dxgw.push({ + name: dxGwName, + stack: await this.stackMapper.dxGwStacks( + `${dxGwName}-dxgw`, + { + namePrefix: dxGwName, + globalPrefix: this.c.global.stackNamePrefix, + ssmParameterPrefix: this.c.global.ssmPrefix, + existingTransitGatewayId: configStanza.existingTgwId, + existingDxGwTransitGatewayAttachId: configStanza.existingDxGwTransitGatewayAttachId, + existingDxGwTransitGatewayRouteTableId: configStanza.existingDxGwTransitGatewayRouteTableId, + tgw: { + attrId: configStanza.existingTgwId + } + } + ), + }); + } + } + async buildEndpointStacks() { for (const endpointName of Object.keys(this.c.providers?.endpoints!)) { const configStanza = this.c.providers!.endpoints![endpointName]; @@ -630,8 +667,8 @@ export class StackBuilderClass { return ""; } - allNamedVpcStacks(): Array { - const allStacks: Array = []; + allNamedStacks(): Array { + const allStacks: Array = []; const cdkStackTypes: Array = [ "providerEndpoint", "providerInternet", @@ -646,10 +683,13 @@ export class StackBuilderClass { this.stacks.vpn.forEach((namedStack) => { allStacks.push(namedStack.stack); }); + this.stacks.dxgw.forEach((namedStack) => { + allStacks.push(namedStack.stack); + }) return allStacks; } - routableStackByName(stackName: string): IBuilderVpc | IBuilderVpn { + routableStackByName(stackName: string): IBuilderVpc | IBuilderVpn | IBuilderDxGw { // Try for a workload stack first, this is the most common try { return this.workloadStackByName("workload", stackName); @@ -669,8 +709,12 @@ export class StackBuilderClass { try { return this.vpnStackByName("vpn", stackName); } catch {} + // Finally a DxGw stack + try { + return this.dxGwStackByName("dxgw", stackName); + } catch {} throw new Error( - `Unable find a workload, or provider VPC with name ${stackName}` + `Unable find a workload, or provider VPC with name ${stackName}` ); } @@ -740,6 +784,19 @@ export class StackBuilderClass { } } + dxGwStackByName(dxGwKey: dxGwKeys, dxGwName: string): IBuilderDxGw { + const dxGwNamedStack = this.stacks[dxGwKey].filter( + (dxGwStack) => dxGwStack.name == dxGwName + )[0]; + if (dxGwNamedStack) { + return dxGwNamedStack.stack; + } else { + throw new Error( + `Unable to find provider type ${dxGwKey} name ${dxGwNamedStack}` + ); + } + } + transitGatewayStackByName( stackKey: transitGatewayKeys, transitGatewayName: string diff --git a/lib/stack-mapper.ts b/lib/stack-mapper.ts index 848f0cf..4fd0c6a 100644 --- a/lib/stack-mapper.ts +++ b/lib/stack-mapper.ts @@ -36,6 +36,10 @@ import { IVpnToTransitGatewayProps, VpnToTransitGatewayStack, } from "./vpn-to-transit-gateway-stack"; +import { + IDirectConnectGatewayProps, + DirectConnectGatewayStack +} from "./direct-connect-gateway-stack" import { IDnsRoute53PrivateHostedZonesProps, DnsRoute53PrivateHostedZonesClass, @@ -49,13 +53,17 @@ export type endpointStackProps = | IVpcRoute53ResolverEndpointsProps; export type internetStackProps = IVpcNatEgressProps; export type transitGatewayStackProps = ITransitGatewayProps; +export type directConnectGatewayProps = IDirectConnectGatewayProps; export interface StackMapperProps {} export class StackMapper { app: cdk.App = new cdk.App(); c: IConfig; - constructor(props?: StackMapperProps) {} + props: StackMapperProps + constructor(props: StackMapperProps) { + this.props = props + } configure(c: IConfig) { this.c = c; @@ -114,6 +122,25 @@ export class StackMapper { } } + async dxGwStacks( + stackName: string, + props: directConnectGatewayProps + ) { + const cfnStackName = + `${this.c.global.stackNamePrefix}-${stackName}`.toLowerCase(); + const stackClass = new DirectConnectGatewayStack( + this.app, + cfnStackName, + props + ); + await stackClass.init(); + stackClass.saveTgwRouteInformation(); + stackClass.attachToTGW(); + stackClass.createSsmParameters(); + this.tagStack(stackClass); + return stackClass; + } + async providerFirewallStacks( style: IBuilderVpcStyle, stackName: string, diff --git a/lib/transit-gateway-routes-stack.ts b/lib/transit-gateway-routes-stack.ts index 6892852..afc4b9e 100644 --- a/lib/transit-gateway-routes-stack.ts +++ b/lib/transit-gateway-routes-stack.ts @@ -4,6 +4,7 @@ import * as ec2 from "aws-cdk-lib/aws-ec2"; import { IBuilderVpc, IBuilderVpn, + IBuilderDxGw, IVpcSubnetParameterNames, IVpcParameterNames, INamedSubnet, @@ -19,7 +20,7 @@ import * as nodeLambda from "aws-cdk-lib/aws-lambda-nodejs"; const md5 = require("md5"); // TODO Add more to this as they are implemented -export type tgwAttachmentsAndRouteTypes = IBuilderVpc | IBuilderVpn; +export type tgwAttachmentsAndRouteTypes = IBuilderVpc | IBuilderVpn | IBuilderDxGw; export interface ITransitGatewayRoutesProps extends cdk.StackProps { tgwAttachmentsAndRoutes: Array; @@ -27,8 +28,8 @@ export interface ITransitGatewayRoutesProps extends cdk.StackProps { } interface tgwSetupStaticOrDefaultRouteProps { - attachable: IBuilderVpc | IBuilderVpn; - routeTo: IBuilderVpc | IBuilderVpn; + attachable: IBuilderVpc | IBuilderVpn | IBuilderDxGw; + routeTo: IBuilderVpc | IBuilderVpn | IBuilderDxGw; inspectBy: IBuilderVpc | IBuilderVpn | undefined; destCidr: string; routeStyle: "static" | "default"; @@ -233,7 +234,7 @@ export class TransitGatewayRoutesStack extends cdk.Stack { // Return: Dest -> Propagation -> Source // Inspect: Forward: Source -> Static CIDR of Dest -> Inspect. Inspect -> Propagation -> Dest. // Return: Dest -> Static CIDR of Source -> Inspect. Inspect -> Propagation -> Source - tgwSetupAttachmentPropagations(attachable: IBuilderVpc | IBuilderVpn) { + tgwSetupAttachmentPropagations(attachable: IBuilderVpc | IBuilderVpn | IBuilderDxGw) { const vpcName = attachable.name; attachable.tgwPropagateRouteAttachmentNames.forEach((attachmentName) => { const attachTo = attachmentName.attachTo; @@ -283,7 +284,7 @@ export class TransitGatewayRoutesStack extends cdk.Stack { }); } - tgwSetupBlackHoles(attachable: IBuilderVpc | IBuilderVpn) { + tgwSetupBlackHoles(attachable: IBuilderVpc | IBuilderVpn | IBuilderDxGw) { const vpcName = attachable.name; attachable.tgwBlackHoleCidrs.forEach((blackHoleCidr) => { const tgwRouteTableId = this.insertSsmToken(attachable.tgwRouteTableSsm); @@ -298,7 +299,7 @@ export class TransitGatewayRoutesStack extends cdk.Stack { } // Configure our default route unless we have a conflicting 'inspected' route. Inspected route should be preferred. - tgwSetupDefaultRoute(attachable: IBuilderVpc | IBuilderVpn) { + tgwSetupDefaultRoute(attachable: IBuilderVpc | IBuilderVpn | IBuilderDxGw) { // Where we have an inspected attachment but also the same attachment as the default route, remove the default route. if (attachable.tgwDefaultRouteAttachmentName) { if ( @@ -322,7 +323,7 @@ export class TransitGatewayRoutesStack extends cdk.Stack { } // Static routes we iterate through multiples, same logic as a default though. - tgwSetupStaticRoutes(attachable: IBuilderVpc | IBuilderVpn) { + tgwSetupStaticRoutes(attachable: IBuilderVpc | IBuilderVpn | IBuilderDxGw) { attachable.tgwStaticRoutes.forEach((staticRoute) => { this.tgwSetupStaticOrDefaultRoute({ attachable: attachable, diff --git a/lib/types.ts b/lib/types.ts index abc0fbd..9baa93c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -23,7 +23,7 @@ export interface ITransitGatewayBaseProps extends cdk.StackProps { /* * Base for anything transit gateway attached that needs to route */ -export type ITgwAttachType = "vpc" | "vpn"; +export type ITgwAttachType = "vpc" | "vpn" | "dxgw"; export interface ITransitGatewayAttachImport { attrId: string; } @@ -131,6 +131,19 @@ export interface IBuilderVpnProps extends IBuilderBaseProps { tunnelTwoOptions?: IConfigVpnTunnelOptions; } +/* + * Base Direct Connect Gateway (DxGw) Class and base properties for our Direct Connect Gateway Imports + * + * NOTE: There is no Cloudformation support for Direct Connect at the moment. This will serve as an abstract model so + * so we can import the tgw attachments and create static routes and propagations + * See: https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/876 + * Expand in the future to support creation of the Dx Gateway itself when support is added. + */ +export interface IBuilderDxGw extends IBuilderBase { +} +export interface IBuilderDxGwProps extends IBuilderBaseProps { +} + export interface ICustomResourceParseAwsFirewallEndpoints { firewallEndpoints: Array; availabilityZone: string; @@ -182,7 +195,7 @@ export interface INamedSubnet { } export interface ITgwPropagateRouteAttachmentName { - attachTo: IBuilderVpc | IBuilderVpn; + attachTo: IBuilderVpc | IBuilderVpn | IBuilderDxGw; inspectBy?: IBuilderVpc | IBuilderVpn; } diff --git a/test/config-parser.test.ts b/test/config-parser.test.ts index d7d378d..d4cfe32 100644 --- a/test/config-parser.test.ts +++ b/test/config-parser.test.ts @@ -431,8 +431,9 @@ test("VpcMarkedNotToAttachHasTgwRoute", () => { ); }); -test("DuplicateVpcVpnAndOrEndpointName", () => { +test("DuplicateResourceNamesUsed", () => { const configContents = minimumConfig(); + // dev provider has same name as the dev vpc configContents.providers = { internet: { dev: { @@ -445,31 +446,55 @@ test("DuplicateVpcVpnAndOrEndpointName", () => { configContents.transitGateways = { testing: { style: "transitGateway", - tgwDescription: "testing", - dynamicRoutes: [ - { - vpcName: "dev", - routesTo: "dev", - }, - ], + tgwDescription: "testing" }, }; let config = new ConfigParser({ configContents: configContents }); expect(() => config.parse()).toThrow( - "Name Providers, VPNs and Vpcs with unique names. Duplicate name dev was found" + "Providers, VPNs, VPCs, and DxGws must be named uniquely within the config file. Duplicate name dev was found" ); delete configContents.providers; + // // dev vpn has same name as the dev vpc configContents.vpns = { dev: { style: "transitGatewayAttached", useTransit: "testing", - existingCustomerGatewayId: "", + existingCustomerGatewayId: "cgw-12345", }, }; + config = new ConfigParser({ configContents: configContents }); + expect(() => config.parse()).toThrow( + "Providers, VPNs, VPCs, and DxGws must be named uniquely within the config file. Duplicate name dev was found" + ); + delete configContents.vpns + // // dev dxgw has same name as the dev vpc + configContents.dxgws = { + dev: { + existingDxGwTransitGatewayAttachId: "tgwattach-1234", + existingDxGwTransitGatewayRouteTableId: "tgw-rtb-1234", + existingTgwId: "tgw-1234", + }, + } + config = new ConfigParser({ configContents: configContents }); + expect(() => config.parse()).toThrow( + "Providers, VPNs, VPCs, and DxGws must be named uniquely within the config file. Duplicate name dev was found" + ); }); -test("RoutesWithNoVpcsOrEndpointsDynamic", () => { - const configContents = minimumConfig(); +// vpcName points to a vpc +// routesTo points to a valid resource +// inspectedBy when configured is a firewall device +// where routesTo is a VPN and inspectBy is configured we redirect to static or dynamic routes +test("RouteNamingSanity", () => { + const routeTypes = [ "blackholeRoutes", "staticRoutes", "dynamicRoutes", "defaultRoutes" ] + + const routeHuman: Record = { + blackholeRoutes: "blackhole route", + staticRoutes: "static route", + dynamicRoutes: "dynamic route", + defaultRoutes: "default route" + } + const configContents: any = minimumConfig(); configContents.providers = { internet: { testing: { @@ -478,51 +503,106 @@ test("RoutesWithNoVpcsOrEndpointsDynamic", () => { useTransit: "testing", }, }, + firewall: { + testingFirewall: { + vpcCidr: "10.1.0.0/17", + style: "awsNetworkFirewall", + useTransit: "testing", + firewallName: "testing", + firewallDescription: "testing", + }, + }, }; configContents.vpcs["dev"].providerInternet = "testing"; + configContents.dxgws = { + todc: { + existingDxGwTransitGatewayAttachId: "tgw-attach-1234", + existingDxGwTransitGatewayRouteTableId: "tgw-rtb-1234", + existingTgwId: "tgw-1234", + }, + } + configContents.vpns = { + devvpn: { + style: "transitGatewayAttached", + useTransit: "testing", + existingCustomerGatewayId: "cgw-12345", + }, + }; configContents.transitGateways = { testing: { style: "transitGateway", tgwDescription: "testing", - dynamicRoutes: [ + }, + }; + routeTypes.forEach((routeType) => { + if(routeType == "blackholeRoutes") { + configContents.transitGateways["testing"][routeType] = [ + { + vpcName: "testing2", + blackholeCidrs: [ "10.1.0.0/16" ], + }, + ] + } + if(routeType == "dynamicRoutes" || routeType == "defaultRoutes") { + configContents.transitGateways["testing"][routeType] = [ { vpcName: "testing2", routesTo: "dev", }, - ], - }, - }; - // vpcName missing - let config = new ConfigParser({ configContents: configContents }); - expect(() => config.parse()).toThrow( - "A dynamic route was specified for testing2 but no vpc, vpn or provider with that name could be found" - ); - // routesTo missing - configContents.transitGateways["testing"].dynamicRoutes?.pop(); - configContents.transitGateways["testing"].dynamicRoutes = [ - { - vpcName: "dev", - routesTo: "testing2", - }, - ]; - expect(() => config.parse()).toThrow( - "A dynamic route was specified for testing2 but no vpc, vpn or provider with that name could be found" - ); - // inspectBy Missing (although matches another provider) - configContents.transitGateways["testing"].dynamicRoutes?.pop(); - configContents.transitGateways["testing"].dynamicRoutes = [ - { - vpcName: "dev", - routesTo: "testing", - inspectedBy: "testing", - }, - ]; - expect(() => config.parse()).toThrow( - "A dynamic route is set to be inspected by testing but no firewall provider with that name was found" - ); + ] + } + if(routeType == "staticRoutes") { + configContents.transitGateways["testing"][routeType] = [ + { + vpcName: "testing2", + routesTo: "dev", + staticCidr: "10.1.0.0/16" + }, + ] + } + // vpcName is not present for any resource + console.log(JSON.stringify(configContents, null, 2)) + let config = new ConfigParser({ configContents: configContents }); + expect(() => config.parse()).toThrow( + `A ${routeHuman[routeType]} was specified for testing2 - vpc with that name could not be found` + ); + // vpcName is a non-vpc resource + configContents.transitGateways["testing"][routeType][0].vpcName = "todc" + config = new ConfigParser({ configContents: configContents }); + expect(() => config.parse()).toThrow( + `Invalid vpcName specified for ${routeHuman[routeType]}. 'vpcName: todc'. A non-VPC resource is using this name.` + ); + if(routeType != "blackholeRoutes") { + // routesTo is not present in config + configContents.transitGateways["testing"][routeType][0].vpcName = "dev" + configContents.transitGateways["testing"][routeType][0].routesTo = "testing2" + config = new ConfigParser({ configContents: configContents }); + expect(() => config.parse()).toThrow( + `A ${routeHuman[routeType]} for VPC Named dev to route to testing2. Configuration file does not contain a resource named testing2` + ); + // inspectBy Missing (although matches another provider). InspectBy not valid for BlackHOle routes + configContents.transitGateways["testing"][routeType][0].routesTo = "testing" + configContents.transitGateways["testing"][routeType][0].inspectedBy = "testing" + config = new ConfigParser({configContents: configContents}); + expect(() => config.parse()).toThrow( + `A ${routeHuman[routeType]} is set to be inspected by testing but no firewall provider with that name was found` + ); + // inspectBy routesTo VPN with dynamic routes + if(routeType == "dynamicRoutes") { + configContents.transitGateways["testing"][routeType][0].routesTo = "devvpn" + configContents.transitGateways["testing"][routeType][0].inspectedBy = "testingFirewall" + config = new ConfigParser({configContents: configContents}); + expect(() => config.parse()).toThrow( + "VPN as the 'routesTo' destination with inspection is not possible using Dynamic Routing. Implement via Static or Default Route instead." + ); + } + } + // Clean out for our next route type tests + delete configContents.transitGateways["testing"][routeType] + }) }); -test("RoutesWithNoVpcsOrEndpointsStatic", () => { +test("RouteToInternetWithNoInternetProviderInVpc", () => { const configContents = minimumConfig(); configContents.providers = { internet: { @@ -533,162 +613,76 @@ test("RoutesWithNoVpcsOrEndpointsStatic", () => { }, }, }; - configContents.vpcs["dev"].providerInternet = "testing"; configContents.transitGateways = { testing: { style: "transitGateway", tgwDescription: "testing", - staticRoutes: [ + defaultRoutes: [ { - vpcName: "testing2", - staticCidr: "10.1.1.0/24", - routesTo: "dev", + vpcName: "dev", + routesTo: "testing", }, ], }, }; - // vpcName missing let config = new ConfigParser({ configContents: configContents }); expect(() => config.parse()).toThrow( - "A static route was specified for testing2 but no vpc, vpn or provider with that name could be found" - ); - // routesTo missing - configContents.transitGateways["testing"].staticRoutes?.pop(); - configContents.transitGateways["testing"].staticRoutes = [ - { - vpcName: "dev", - staticCidr: "10.1.1.0/24", - routesTo: "testing2", - }, - ]; - expect(() => config.parse()).toThrow( - "A static route was specified for testing2 but no vpc, vpn or provider with that name could be found" - ); - // inspectBy Missing (although matches another provider) - configContents.transitGateways["testing"].staticRoutes?.pop(); - configContents.transitGateways["testing"].staticRoutes = [ - { - vpcName: "dev", - staticCidr: "10.1.1.0/24", - routesTo: "testing", - inspectedBy: "testing", - }, - ]; - expect(() => config.parse()).toThrow( - "A static route is set to be inspected by testing but no firewall provider with that name was found" + "Vpc: dev has a route to internet provider testing but does not have 'providerInternet' defined in the vpc configuration." ); }); -test("RoutesWithNoVpcsOrEndpointsDefault", () => { +test("DxGwImportValuesNotCorrect", () => { const configContents = minimumConfig(); - configContents.providers = { - internet: { - testing: { - vpcCidr: "10.1.0.0/17", - style: "natEgress", - useTransit: "testing", - }, + configContents.dxgws = { + toDc: { + existingDxGwTransitGatewayAttachId: "tgwattach-1234", + existingDxGwTransitGatewayRouteTableId: "tgw-rtb-1234", + existingTgwId: "tgw-1234", }, }; - configContents.vpcs["dev"].providerInternet = "testing"; configContents.transitGateways = { testing: { style: "transitGateway", tgwDescription: "testing", - defaultRoutes: [ - { - vpcName: "testing2", - routesTo: "dev", - }, - ], }, }; - // vpcName missing let config = new ConfigParser({ configContents: configContents }); expect(() => config.parse()).toThrow( - "A default route was specified for testing2 but no vpc, vpn or provider with that name could be found" - ); - // routesTo missing - configContents.transitGateways["testing"].defaultRoutes?.pop(); - configContents.transitGateways["testing"].defaultRoutes = [ - { - vpcName: "dev", - routesTo: "testing2", - }, - ]; - expect(() => config.parse()).toThrow( - "A default route was specified for testing2 but no vpc, vpn or provider with that name could be found" + "DxGw: toDc: Transit Gateway Attachment Value 'existingDxGwTransitGatewayAttachId' must begin with tgw-attach-" ); - // inspectBy Missing (although matches another provider) - configContents.transitGateways["testing"].defaultRoutes?.pop(); - configContents.transitGateways["testing"].defaultRoutes = [ - { - vpcName: "dev", - routesTo: "testing", - inspectedBy: "testing", - }, - ]; - expect(() => config.parse()).toThrow( - "A default route is set to be inspected by testing but no firewall provider with that name was found" - ); -}); - -test("RoutesWithNoVpcsOrEndpointsBlackhole", () => { - const configContents = minimumConfig(); - configContents.providers = { - internet: { - testing: { - vpcCidr: "10.1.0.0/17", - style: "natEgress", - useTransit: "testing", - }, + configContents.dxgws = { + toDc: { + existingDxGwTransitGatewayAttachId: "tgw-attach-1234", + existingDxGwTransitGatewayRouteTableId: "tgwrtb-1234", + existingTgwId: "tgw-1234", }, }; configContents.transitGateways = { testing: { style: "transitGateway", tgwDescription: "testing", - blackholeRoutes: [ - { - vpcName: "testing2", - blackholeCidrs: ["10.1.1.0/24"], - }, - ], }, }; - // vpcName missing - let config = new ConfigParser({ configContents: configContents }); + config = new ConfigParser({ configContents: configContents }); expect(() => config.parse()).toThrow( - "A blackhole route was specified for testing2 but no vpc, vpn or provider with that name could be found" + "DxGw: toDc: Transit Gateway Route Table Value 'existingDxGwTransitGatewayRouteTableId' must begin with tgw-rtb-" ); -}); - -test("RouteToInternetWithNoInternetProviderInVpc", () => { - const configContents = minimumConfig(); - configContents.providers = { - internet: { - testing: { - vpcCidr: "10.1.0.0/17", - style: "natEgress", - useTransit: "testing", - }, + configContents.dxgws = { + toDc: { + existingDxGwTransitGatewayAttachId: "tgw-attach-1234", + existingDxGwTransitGatewayRouteTableId: "tgw-rtb-1234", + existingTgwId: "tgwid-1234", }, }; configContents.transitGateways = { testing: { style: "transitGateway", tgwDescription: "testing", - defaultRoutes: [ - { - vpcName: "dev", - routesTo: "testing", - }, - ], }, }; - let config = new ConfigParser({ configContents: configContents }); + config = new ConfigParser({ configContents: configContents }); expect(() => config.parse()).toThrow( - "Vpc: dev has a route to internet provider testing but does not have 'providerInternet' defined in the vpc configuration." + "DxGw: toDc: Existing Transit Gateway 'existingTgwId' must begin with tgw-" ); }); @@ -849,87 +843,6 @@ test("VpnCorrectViaImportMissing", () => { ); }); -test("VpnRouteOptionsInvalid", () => { - const configContents = minimumConfig(); - configContents.vpns = { - onPrem: { - style: "transitGatewayAttached", - useTransit: "testing", - existingCustomerGatewayId: "cgw-12345", - }, - }; - configContents.transitGateways = { - testing: { - style: "transitGateway", - tgwDescription: "testing", - dynamicRoutes: [ - { - vpcName: "onPrem", - routesTo: "dev", - }, - ], - }, - }; - let config = new ConfigParser({ configContents: configContents }); - expect(() => config.parse()).toThrow( - "A dynamic route was specified with onPrem. This is a VPN and cannot be in the 'vpcName' field. You can only 'routeTo' VPNs" - ); - delete configContents.transitGateways["testing"].dynamicRoutes; - configContents.transitGateways["testing"].staticRoutes = [ - { - vpcName: "onPrem", - staticCidr: "10.1.1.0/24", - routesTo: "dev", - }, - ]; - expect(() => config.parse()).toThrow( - "A static route was specified with onPrem. This is a VPN and cannot be in the 'vpcName' field. You can only 'routeTo' VPNs" - ); - delete configContents.transitGateways["testing"].staticRoutes; - configContents.transitGateways["testing"].blackholeRoutes = [ - { - vpcName: "onPrem", - blackholeCidrs: ["10.1.1.0/24"], - }, - ]; - expect(() => config.parse()).toThrow( - "A blackhole route was specified with onPrem. This is a VPN and cannot be in the 'vpcName' field. You can only 'routeTo' VPNs" - ); - delete configContents.transitGateways["testing"].blackholeRoutes; - configContents.transitGateways["testing"].defaultRoutes = [ - { - vpcName: "onPrem", - routesTo: "dev", - }, - ]; - expect(() => config.parse()).toThrow( - "A default route was specified with onPrem. This is a VPN and cannot be in the 'vpcName' field. You can only 'routeTo' VPNs" - ); - configContents.providers = { - firewall: { - testingFirewall: { - vpcCidr: "10.1.0.0/17", - style: "awsNetworkFirewall", - useTransit: "testing", - firewallName: "testing", - firewallDescription: "testing", - }, - }, - }; - // Dynamic Routing with inspection via firewall to the VPN is not available. - delete configContents.transitGateways["testing"].defaultRoutes; - configContents.transitGateways["testing"].dynamicRoutes = [ - { - vpcName: "dev", - routesTo: "onPrem", - inspectedBy: "testingFirewall", - }, - ]; - expect(() => config.parse()).toThrow( - "VPN inspection is not possible via Dynamic Routing. Implement via Static or Default Route instead." - ); -}); - test("VpnMissingTransit", () => { const configContents = minimumConfig(); configContents.vpns = { diff --git a/test/direct-connect-gateway-stack.test.ts b/test/direct-connect-gateway-stack.test.ts new file mode 100644 index 0000000..2bcefe4 --- /dev/null +++ b/test/direct-connect-gateway-stack.test.ts @@ -0,0 +1,24 @@ +import { Template } from "aws-cdk-lib/assertions"; +import { newDxGwStack } from "./stack-builder-helper"; +import * as cdk from "aws-cdk-lib"; + +// This stack is just a placeholder for SSM parameters to support our Transit Gateway Route creation etc. +// Confirm our SSM parameters are created and the correct path/value. +test("SsmParametersCreated", () => { + const app = new cdk.App(); + const dxStack = newDxGwStack({}, app) + dxStack.saveTgwRouteInformation(); + dxStack.attachToTGW(); + dxStack.createSsmParameters(); + const template = Template.fromStack(dxStack); + + // We expect SSM Exports that our stacks above can consume: + template.hasResourceProperties("AWS::SSM::Parameter", { + Name: "/ssm/prefix/networking/globalprefix/dxgw/test-dxgw/tgwRouteId", + Value: "tgw-rtb-12345", + }); + template.hasResourceProperties("AWS::SSM::Parameter", { + Name: "/ssm/prefix/networking/globalprefix/dxgw/test-dxgw/tgwAttachId", + Value: "tgw-attach-12345", + }); +}) diff --git a/test/stack-builder-helper.ts b/test/stack-builder-helper.ts index 1ddf827..b146628 100644 --- a/test/stack-builder-helper.ts +++ b/test/stack-builder-helper.ts @@ -1,11 +1,7 @@ import { - IBuilderVpcStyle, - ITransitGatewayStyle, - IBuilderVpnStyle, IVpcWorkloadProps, ITgw, } from "../lib/types"; -import { IConfig } from "../lib/config/config-types"; import { TransitGatewayStack } from "../lib/transit-gateway-stack"; import { IVpcInterfaceEndpointsProps, @@ -29,6 +25,10 @@ import { IVpnToTransitGatewayProps, VpnToTransitGatewayStack, } from "../lib/vpn-to-transit-gateway-stack"; +import { + IDirectConnectGatewayProps, + DirectConnectGatewayStack, +} from "../lib/direct-connect-gateway-stack"; import * as cdk from "aws-cdk-lib"; import * as ec2 from "aws-cdk-lib/aws-ec2"; import * as fs from "fs"; @@ -208,6 +208,27 @@ export const newVpnStack = ( ); }; +export const newDxGwStack = ( + props: Partial, + app: cdk.App, +) => { + const commonProps: IDirectConnectGatewayProps = { + globalPrefix: "globalPrefix", + ssmParameterPrefix: "/ssm/prefix", + namePrefix: "Test", + existingDxGwTransitGatewayAttachId: "tgw-attach-12345", + existingDxGwTransitGatewayRouteTableId: "tgw-rtb-12345", + existingTransitGatewayId: "tgw-12345", + ...props, + }; + + return new DirectConnectGatewayStack( + app, + `${props.namePrefix}VpnToTransitGatewayStack`, + commonProps + ); +}; + export const newVpcRoute53ResolverStack = ( props: Partial, app: cdk.App, diff --git a/test/transit-gateway-routes-stack-tgw-routes.test.ts b/test/transit-gateway-routes-stack-tgw-routes.test.ts index 6708d0f..00a624a 100644 --- a/test/transit-gateway-routes-stack-tgw-routes.test.ts +++ b/test/transit-gateway-routes-stack-tgw-routes.test.ts @@ -1,13 +1,12 @@ -import { Template, Match } from "aws-cdk-lib/assertions"; +import { Template } from "aws-cdk-lib/assertions"; import { TransitGatewayRoutesStack, - ITransitGatewayRoutesProps, } from "../lib/transit-gateway-routes-stack"; import { newAwsNetworkFirewallStack, newVpcWorkloadStack, + newDxGwStack } from "./stack-builder-helper"; -import { IBuilderVpc } from "../lib/types"; import * as cdk from "aws-cdk-lib"; const md5 = require("md5"); @@ -41,6 +40,15 @@ const twoWorkloadVpcs = (app: cdk.App) => { return [firstVpc, secondVpc]; }; +const createDxGw = (app: cdk.App) => { + const dxStack = newDxGwStack({}, app) + dxStack.saveTgwRouteInformation(); + dxStack.attachToTGW(); + dxStack.createSsmParameters(); + + return dxStack +}; + // Black Hole Routes test("TgwRouteBlackhole", () => { const app = new cdk.App(); @@ -103,6 +111,44 @@ test("TgwRouteDynamic", () => { }); }); +// A dynamic route from a VPC to a DxGw (a propagation) +test("TgwRouteDynamicToDxGw", () => { + const app = new cdk.App(); + const [firstVpc, secondVpc] = twoWorkloadVpcs(app); + + const dxgw = createDxGw(app) + // Set our relationship between the VPCs + firstVpc.tgwPropagateRouteAttachmentNames.push({ + attachTo: dxgw, + }); + + const routeStack = new TransitGatewayRoutesStack(app, "RouteStack", { + tgwAttachmentsAndRoutes: [firstVpc, secondVpc, dxgw], + }); + + const template = Template.fromStack(routeStack); + const templateJson = template.toJSON(); + // console.log(JSON.stringify(templateJson, null, 2)) + // Confirm we get an association both ways + const firstRouteId = + `TGWPropRoute${firstVpc.name}to${dxgw.name}`.replace(/-/g, ""); + expect(templateJson.Resources).toMatchObject({ + [firstRouteId]: { + Type: "AWS::EC2::TransitGatewayRouteTablePropagation", + Properties: expect.anything(), + }, + }); + + const secondRouteId = + `TGWPropRoute${firstVpc.name}to${dxgw.name}`.replace(/-/g, ""); + expect(templateJson.Resources).toMatchObject({ + [secondRouteId]: { + Type: "AWS::EC2::TransitGatewayRouteTablePropagation", + Properties: expect.anything(), + }, + }); +}); + // A static route between VPCs test("TgwStaticDynamic", () => { const app = new cdk.App(); @@ -134,6 +180,38 @@ test("TgwStaticDynamic", () => { }); }); +// A static route to a DxGw +test("TgwStaticVpcToDxGw", () => { + const app = new cdk.App(); + const [firstVpc, secondVpc] = twoWorkloadVpcs(app); + + const dxgw = createDxGw(app) + // Set our relationship between the VPCs + firstVpc.tgwStaticRoutes.push({ + cidrAddress: "10.1.2.1/24", + attachTo: dxgw, + }); + + const routeStack = new TransitGatewayRoutesStack(app, "RouteStack", { + tgwAttachmentsAndRoutes: [firstVpc, secondVpc, dxgw], + }); + + const template = Template.fromStack(routeStack); + const templateJson = template.toJSON(); + + // Confirm our static route is in place and pointing to our custom resource for handling + const firstRouteId = + "StaticRouteCR" + md5(`${firstVpc.name}-10.1.2.1/24-${dxgw.name}`); + expect(templateJson.Resources).toMatchObject({ + [firstRouteId]: { + Type: "AWS::CloudFormation::CustomResource", + Properties: { + destinationCidrBlock: "10.1.2.1/24", + }, + }, + }); +}); + // A default route between VPCs test("TgwStatic", () => { const app = new cdk.App(); @@ -163,6 +241,37 @@ test("TgwStatic", () => { }); }); +// A default route from a VPC to a DxGw +test("TgwDefaultRouteVpcToDxGw", () => { + const app = new cdk.App(); + const [firstVpc, secondVpc] = twoWorkloadVpcs(app); + + const dxgw = createDxGw(app) + // Set our relationship between the VPCs + firstVpc.tgwDefaultRouteAttachmentName = { + attachTo: dxgw, + }; + + const routeStack = new TransitGatewayRoutesStack(app, "RouteStack", { + tgwAttachmentsAndRoutes: [firstVpc, secondVpc, dxgw], + }); + + const template = Template.fromStack(routeStack); + const templateJson = template.toJSON(); + // Confirm our default route is in place + const firstRouteId = "TGWDefaultCR" + md5(firstVpc.tgwRouteTableSsm.name); + expect(templateJson.Resources).toMatchObject({ + [firstRouteId]: { + Type: "AWS::CloudFormation::CustomResource", + Properties: { + destinationCidrBlock: "0.0.0.0/0", + // this is the attachment identifier of the DxGw + transitGatewayAttachmentId: "tgw-attach-12345" + }, + }, + }); +}); + // A dynamic route between VPCs that is inspected // Inspect: Forward: Source -> Static CIDR of Dest -> Inspect. Inspect -> Propagation -> Dest. // Return: Dest -> Static CIDR of Source -> Inspect. Inspect -> Propagation -> Source @@ -403,8 +512,6 @@ test("TgwDefaultOverridesDynamic", () => { }, }); // Dynamic route should not exist - const secondRouteId = - `TGWPropRoute${firstVpc.name}to${secondVpc.name}`.replace(/-/g, ""); expect(templateJson.Resources).not.toMatchObject({ [firstRouteId]: { Type: "AWS::EC2::TransitGatewayRouteTablePropagation",