diff --git a/docs/src/context.rst b/docs/src/context.rst index 1ea6441a77dca..f35ac42f16d15 100644 --- a/docs/src/context.rst +++ b/docs/src/context.rst @@ -42,3 +42,16 @@ The |cdk| currently supports the following context providers. .. code:: js const ami: string = new SSMParameterProvider(this).getString('my-awesome-value'); + +:py:class:`VpcNetworkProvider <@aws-cdk/aws-ec2.VpcNetworkProvider>` + Use this provider to look up and reference existing VPC in your accounts. + For example, the follow code imports a VPC by tag name: + +.. code:: js + + const provider = new VpcNetworkProvider(this, { + tags: { + Purpose: 'WebServices' + } + }); + const vpc = VpcNetworkRef.import(this, 'VPC', provider.vpcProps); diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index bbb3b6fd7a59e..4cc2a36b0c183 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -172,6 +172,18 @@ The `VpcNetwork` above will have the exact same subnet definitions as listed above. However, this time the VPC will have only 1 NAT Gateway and all Application subnets will route to the NAT Gateway. +#### Sharing VPCs across stacks + +If you are creating multiple `Stack`s inside the same CDK application, +you can reuse a VPC from one Stack in another by using `export()` and +`import()`: + +[sharing VPCs between stacks](test/example.share-vpcs.lit.ts) + +If your VPC is created outside your CDK app, you can use `importFromContext()`: + +[importing existing VPCs](test/integ.import-default-vpc.lit.ts) + ### Allowing Connections In AWS, all network traffic in and out of **Elastic Network Interfaces** (ENIs) diff --git a/packages/@aws-cdk/aws-ec2/lib/index.ts b/packages/@aws-cdk/aws-ec2/lib/index.ts index 9e6e38d27bba0..bade088217cae 100644 --- a/packages/@aws-cdk/aws-ec2/lib/index.ts +++ b/packages/@aws-cdk/aws-ec2/lib/index.ts @@ -5,6 +5,7 @@ export * from './security-group'; export * from './security-group-rule'; export * from './vpc'; export * from './vpc-ref'; +export * from './vpc-network-provider'; // AWS::EC2 CloudFormation Resources: export * from './ec2.generated'; diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-network-provider.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-network-provider.ts new file mode 100644 index 0000000000000..3268e9a90d0ce --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-network-provider.ts @@ -0,0 +1,76 @@ +import cdk = require('@aws-cdk/cdk'); +import cxapi = require('@aws-cdk/cx-api'); +import { VpcNetworkRefProps } from './vpc-ref'; + +/** + * Properties for looking up an existing VPC. + * + * The combination of properties must specify filter down to exactly one + * non-default VPC, otherwise an error is raised. + */ +export interface VpcNetworkProviderProps { + /** + * The ID of the VPC + * + * If given, will import exactly this VPC. + * + * @default Don't filter on vpcId + */ + vpcId?: string; + + /** + * The name of the VPC + * + * If given, will import the VPC with this name. + * + * @default Don't filter on vpcName + */ + vpcName?: string; + + /** + * Tags on the VPC + * + * The VPC must have all of these tags + * + * @default Don't filter on tags + */ + tags?: {[key: string]: string}; + + /** + * Whether to match the default VPC + * + * @default Don't care whether we return the default VPC + */ + isDefault?: boolean; +} + +/** + * Context provider to discover and import existing VPCs + */ +export class VpcNetworkProvider { + private provider: cdk.ContextProvider; + + constructor(context: cdk.Construct, props: VpcNetworkProviderProps) { + this.provider = new cdk.ContextProvider(context, cxapi.VPC_PROVIDER, props as cxapi.VpcContextQuery); + } + + /** + * Return the VPC import props matching the filter + */ + public get vpcProps(): VpcNetworkRefProps { + const ret: cxapi.VpcContextResponse = this.provider.getValue(DUMMY_VPC_PROPS); + return ret; + } +} + +/** + * There are returned when the provider has not supplied props yet + * + * It's only used for testing and on the first run-through. + */ +const DUMMY_VPC_PROPS: cxapi.VpcContextResponse = { + availabilityZones: ['dummy-1a', 'dummy-1b'], + vpcId: 'vpc-12345', + publicSubnetIds: ['s-12345', 's-67890'], + privateSubnetIds: ['p-12345', 'p-67890'], +}; diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts index 59d0381c92f39..9833283d2c7a4 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts @@ -1,5 +1,6 @@ import { Construct, IDependable, Output } from "@aws-cdk/cdk"; import { ExportSubnetGroup, ImportSubnetGroup, subnetName } from './util'; +import { VpcNetworkProvider, VpcNetworkProviderProps } from './vpc-network-provider'; /** * The type of Subnet @@ -81,6 +82,13 @@ export abstract class VpcNetworkRef extends Construct implements IDependable { return new ImportedVpcNetwork(parent, name, props); } + /** + * Import an existing VPC from context + */ + public static importFromContext(parent: Construct, name: string, props: VpcNetworkProviderProps): VpcNetworkRef { + return VpcNetworkRef.import(parent, name, new VpcNetworkProvider(parent, props).vpcProps); + } + /** * Identifier for this VPC */ diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 41bc9b97bcfdc..8483fc47e4c7c 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -407,24 +407,46 @@ export class VpcNetwork extends VpcNetworkRef implements cdk.ITaggable { tags: subnetConfig.tags, }; + let subnet: VpcSubnet; switch (subnetConfig.subnetType) { case SubnetType.Public: const publicSubnet = new VpcPublicSubnet(this, name, subnetProps); this.publicSubnets.push(publicSubnet); + subnet = publicSubnet; break; case SubnetType.Private: const privateSubnet = new VpcPrivateSubnet(this, name, subnetProps); this.privateSubnets.push(privateSubnet); + subnet = privateSubnet; break; case SubnetType.Isolated: const isolatedSubnet = new VpcPrivateSubnet(this, name, subnetProps); + isolatedSubnet.tags.setTag(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType)); this.isolatedSubnets.push(isolatedSubnet); + subnet = isolatedSubnet; break; + default: + throw new Error(`Unrecognized subnet type: ${subnetConfig.subnetType}`); } + + // These values will be used to recover the config upon provider import + subnet.tags.setTag(SUBNETNAME_TAG, subnetConfig.name); + subnet.tags.setTag(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType)); }); } } +const SUBNETTYPE_TAG = 'aws-cdk:SubnetType'; +const SUBNETNAME_TAG = 'aws-cdk:SubnetName'; + +function subnetTypeTagValue(type: SubnetType) { + switch (type) { + case SubnetType.Public: return 'Public'; + case SubnetType.Private: return 'Private'; + case SubnetType.Isolated: return 'Isolated'; + } +} + /** * Specify configuration parameters for a VPC subnet */ diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 47a6af8b806a9..cd37a9628d679 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -60,10 +60,12 @@ }, "dependencies": { "@aws-cdk/aws-iam": "^0.16.0", + "@aws-cdk/cx-api": "^0.16.0", "@aws-cdk/cdk": "^0.16.0" }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { + "@aws-cdk/cx-api": "^0.16.0", "@aws-cdk/cdk": "^0.16.0" } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/example.share-vpcs.lit.ts b/packages/@aws-cdk/aws-ec2/test/example.share-vpcs.lit.ts new file mode 100644 index 0000000000000..7cdc7fdced354 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/example.share-vpcs.lit.ts @@ -0,0 +1,40 @@ +import cdk = require('@aws-cdk/cdk'); +import ec2 = require("../lib"); + +const app = new cdk.App(); + +/// !show +class Stack1 extends cdk.Stack { + public readonly vpcProps: ec2.VpcNetworkRefProps; + + constructor(parent: cdk.App, id: string, props?: cdk.StackProps) { + super(parent, id, props); + + const vpc = new ec2.VpcNetwork(this, 'VPC'); + + // Export the VPC to a set of properties + this.vpcProps = vpc.export(); + } +} + +interface Stack2Props extends cdk.StackProps { + vpcProps: ec2.VpcNetworkRefProps; +} + +class Stack2 extends cdk.Stack { + constructor(parent: cdk.App, id: string, props: Stack2Props) { + super(parent, id, props); + + // Import the VPC from a set of properties + const vpc = ec2.VpcNetworkRef.import(this, 'VPC', props.vpcProps); + } +} + +const stack1 = new Stack1(app, 'Stack1'); +const stack2 = new Stack2(app, 'Stack2', { + vpcProps: stack1.vpcProps +}); +/// !hide + +app.run(); + diff --git a/packages/@aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.expected.json new file mode 100644 index 0000000000000..37edfef83075b --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.expected.json @@ -0,0 +1,33 @@ +{ + "Resources": { + "SecurityGroupDD263621": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-cdk-ec2-import/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "VpcId": "vpc-60900905" + } + } + }, + "Outputs": { + "PublicSubnets": { + "Value": "ids:subnet-e19455ca,subnet-e0c24797,subnet-ccd77395", + "Export": { + "Name": "aws-cdk-ec2-import:PublicSubnets" + } + }, + "PrivateSubnets": { + "Value": "ids:", + "Export": { + "Name": "aws-cdk-ec2-import:PrivateSubnets" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.ts b/packages/@aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.ts new file mode 100644 index 0000000000000..2a9fb8921c65f --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.ts @@ -0,0 +1,24 @@ +import cdk = require('@aws-cdk/cdk'); +import ec2 = require("../lib"); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-ec2-import'); + +/// !show +const vpc = ec2.VpcNetworkRef.importFromContext(stack, 'VPC', { + // This imports the default VPC but you can also + // specify a 'vpcName' or 'tags'. + isDefault: true +}); +/// !hide + +// The only thing in this library that takes a VPC as an argument :) +new ec2.SecurityGroup(stack, 'SecurityGroup', { + vpc +}); + +// Try subnet selection +new cdk.Output(stack, 'PublicSubnets', { value: 'ids:' + vpc.subnets({ subnetsToUse: ec2.SubnetType.Public }).map(s => s.subnetId).join(',') }); +new cdk.Output(stack, 'PrivateSubnets', { value: 'ids:' + vpc.subnets({ subnetsToUse: ec2.SubnetType.Private }).map(s => s.subnetId).join(',') }); + +app.run(); diff --git a/packages/@aws-cdk/cx-api/lib/context/vpc.ts b/packages/@aws-cdk/cx-api/lib/context/vpc.ts new file mode 100644 index 0000000000000..7eb832a77a461 --- /dev/null +++ b/packages/@aws-cdk/cx-api/lib/context/vpc.ts @@ -0,0 +1,21 @@ +export const VPC_PROVIDER = 'vpc-provider'; + +export interface VpcContextQuery { + region: string; + account: string; + vpcId?: string; + vpcName?: string; + tags?: {[key: string]: string}; + isDefault?: boolean; +} + +export interface VpcContextResponse { + vpcId: string; + availabilityZones: string[]; + publicSubnetIds?: string[]; + publicSubnetNames?: string[]; + privateSubnetIds?: string[]; + privateSubnetNames?: string[]; + isolatedSubnetIds?: string[]; + isolatedSubnetNames?: string[]; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/index.ts b/packages/@aws-cdk/cx-api/lib/index.ts index 6077a01aa9581..e788c1b1ba995 100644 --- a/packages/@aws-cdk/cx-api/lib/index.ts +++ b/packages/@aws-cdk/cx-api/lib/index.ts @@ -1,2 +1,3 @@ export * from './cxapi'; export * from './environment'; +export * from './context/vpc'; diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 3e36a8a4ed68a..3ea0c910d38ef 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -10,7 +10,7 @@ import yargs = require('yargs'); import cdkUtil = require('../lib/util'); import { bootstrapEnvironment, deployStack, destroyStack, loadToolkitInfo, Mode, SDK } from '../lib'; -import contextplugins = require('../lib/contextplugins'); +import contextproviders = require('../lib/context-providers/index'); import { printStackDiff } from '../lib/diff'; import { execProgram } from '../lib/exec'; import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init'; @@ -115,12 +115,6 @@ async function initCommandLine() { ec2creds: argv.ec2creds, }); - const availableContextProviders: contextplugins.ProviderMap = { - 'availability-zones': new contextplugins.AZContextProviderPlugin(aws), - 'ssm': new contextplugins.SSMContextProviderPlugin(aws), - 'hosted-zone': new contextplugins.HostedZoneContextProviderPlugin(aws), - }; - const defaultConfig = new Settings({ versionReporting: true }); const userConfig = await new Settings().load(PER_USER_DEFAULTS); const projectConfig = await new Settings().load(DEFAULTS); @@ -384,7 +378,7 @@ async function initCommandLine() { if (!cdkUtil.isEmpty(allMissing)) { debug(`Some context information is missing. Fetching...`); - await contextplugins.provideContextValues(allMissing, projectConfig, availableContextProviders); + await contextproviders.provideContextValues(allMissing, projectConfig, aws); // Cache the new context to disk await projectConfig.save(DEFAULTS); diff --git a/packages/aws-cdk/lib/context-providers/availability-zones.ts b/packages/aws-cdk/lib/context-providers/availability-zones.ts new file mode 100644 index 0000000000000..f6f80c60150c3 --- /dev/null +++ b/packages/aws-cdk/lib/context-providers/availability-zones.ts @@ -0,0 +1,22 @@ +import { Mode, SDK } from '../api'; +import { debug } from '../logging'; +import { ContextProviderPlugin } from './provider'; + +/** + * Plugin to retrieve the Availability Zones for the current account + */ +export class AZContextProviderPlugin implements ContextProviderPlugin { + constructor(private readonly aws: SDK) { + } + + public async getValue(args: {[key: string]: any}) { + const region = args.region; + const account = args.account; + debug(`Reading AZs for ${account}:${region}`); + const ec2 = await this.aws.ec2(account, region, Mode.ForReading); + const response = await ec2.describeAvailabilityZones().promise(); + if (!response.AvailabilityZones) { return []; } + const azs = response.AvailabilityZones.filter(zone => zone.State === 'available').map(zone => zone.ZoneName); + return azs; + } +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/contextplugins.ts b/packages/aws-cdk/lib/context-providers/hosted-zones.ts similarity index 52% rename from packages/aws-cdk/lib/contextplugins.ts rename to packages/aws-cdk/lib/context-providers/hosted-zones.ts index 8af2987a3d214..6ae4c7f7c537d 100644 --- a/packages/aws-cdk/lib/contextplugins.ts +++ b/packages/aws-cdk/lib/context-providers/hosted-zones.ts @@ -1,57 +1,6 @@ -import cxapi = require('@aws-cdk/cx-api'); -import { Mode, SDK } from './api'; -import { debug } from './logging'; -import { Settings } from './settings'; - -export interface ContextProviderPlugin { - getValue(args: {[key: string]: any}): Promise; -} - -export type ProviderMap = {[name: string]: ContextProviderPlugin}; - -/** - * Plugin to retrieve the Availability Zones for the current account - */ -export class AZContextProviderPlugin implements ContextProviderPlugin { - constructor(private readonly aws: SDK) { - } - - public async getValue(args: {[key: string]: any}) { - const region = args.region; - const account = args.account; - debug(`Reading AZs for ${account}:${region}`); - const ec2 = await this.aws.ec2(account, region, Mode.ForReading); - const response = await ec2.describeAvailabilityZones().promise(); - if (!response.AvailabilityZones) { return []; } - const azs = response.AvailabilityZones.filter(zone => zone.State === 'available').map(zone => zone.ZoneName); - return azs; - } -} - -/** - * Plugin to read arbitrary SSM parameter names - */ -export class SSMContextProviderPlugin implements ContextProviderPlugin { - constructor(private readonly aws: SDK) { - } - - public async getValue(args: {[key: string]: any}) { - const region = args.region; - const account = args.account; - if (!('parameterName' in args)) { - throw new Error('parameterName must be provided in props for SSMContextProviderPlugin'); - } - const parameterName = args.parameterName; - debug(`Reading SSM parameter ${account}:${region}:${parameterName}`); - - const ssm = await this.aws.ssm(account, region, Mode.ForReading); - const response = await ssm.getParameter({ Name: parameterName }).promise(); - if (!response.Parameter || response.Parameter.Value === undefined) { - throw new Error(`SSM parameter not available in account ${account}, region ${region}: ${parameterName}`); - } - return response.Parameter.Value; - } -} +import { Mode, SDK } from '../api'; +import { debug } from '../logging'; +import { ContextProviderPlugin } from './provider'; export interface HostedZoneProviderProps { /** @@ -82,7 +31,7 @@ export class HostedZoneContextProviderPlugin implements ContextProviderPlugin { const account = args.account; const region = args.region; if (!this.isHostedZoneProps(args)) { - throw new Error(`HostedZoneProvider requires domainName property to be set in ${args.props}`); + throw new Error(`HostedZoneProvider requires domainName property to be set in ${args}`); } const domainName = args.domainName; debug(`Reading hosted zone ${account}:${region}:${domainName}`); @@ -133,24 +82,4 @@ export class HostedZoneContextProviderPlugin implements ContextProviderPlugin { private isHostedZoneProps(props: HostedZoneProviderProps | any): props is HostedZoneProviderProps { return (props as HostedZoneProviderProps).domainName !== undefined; } -} -/** - * Iterate over the list of missing context values and invoke the appropriate providers from the map to retrieve them - */ -export async function provideContextValues( - missingValues: { [key: string]: cxapi.MissingContext }, - projectConfig: Settings, - availableContextProviders: ProviderMap) { - for (const key of Object.keys(missingValues)) { - const missingContext = missingValues[key]; - - const provider = availableContextProviders[missingContext.provider]; - if (!provider) { - throw new Error(`Unrecognized context provider name: ${missingContext.provider}`); - } - - const value = await provider.getValue(missingContext.props); - projectConfig.set(['context', key], value); - debug(`Setting "${key}" context to ${JSON.stringify(value)}`); - } -} +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/context-providers/index.ts b/packages/aws-cdk/lib/context-providers/index.ts new file mode 100644 index 0000000000000..84b842f268090 --- /dev/null +++ b/packages/aws-cdk/lib/context-providers/index.ts @@ -0,0 +1,43 @@ +import cxapi = require('@aws-cdk/cx-api'); +import { SDK } from '../api/util/sdk'; +import { debug } from '../logging'; +import { Settings } from '../settings'; +import { AZContextProviderPlugin } from './availability-zones'; +import { HostedZoneContextProviderPlugin } from './hosted-zones'; +import { ContextProviderPlugin } from './provider'; +import { SSMContextProviderPlugin } from './ssm-parameters'; +import { VpcNetworkContextProviderPlugin } from './vpcs'; + +type ProviderConstructor = (new (sdk: SDK) => ContextProviderPlugin); +export type ProviderMap = {[name: string]: ProviderConstructor}; + +/** + * Iterate over the list of missing context values and invoke the appropriate providers from the map to retrieve them + */ +export async function provideContextValues( + missingValues: { [key: string]: cxapi.MissingContext }, + projectConfig: Settings, + sdk: SDK) { + for (const key of Object.keys(missingValues)) { + const missingContext = missingValues[key]; + + const constructor = availableContextProviders[missingContext.provider]; + if (!constructor) { + // tslint:disable-next-line:max-line-length + throw new Error(`Unrecognized context provider name: ${missingContext.provider}. You might need to update the toolkit to match the version of the construct library.`); + } + + const provider = new constructor(sdk); + + const value = await provider.getValue(missingContext.props); + projectConfig.set(['context', key], value); + debug(`Setting "${key}" context to ${JSON.stringify(value)}`); + } +} + +const availableContextProviders: ProviderMap = { + 'availability-zones': AZContextProviderPlugin, + 'ssm': SSMContextProviderPlugin, + 'hosted-zone': HostedZoneContextProviderPlugin, + [cxapi.VPC_PROVIDER]: VpcNetworkContextProviderPlugin, +}; diff --git a/packages/aws-cdk/lib/context-providers/provider.ts b/packages/aws-cdk/lib/context-providers/provider.ts new file mode 100644 index 0000000000000..3d8a59938e9ef --- /dev/null +++ b/packages/aws-cdk/lib/context-providers/provider.ts @@ -0,0 +1,3 @@ +export interface ContextProviderPlugin { + getValue(args: {[key: string]: any}): Promise; +} diff --git a/packages/aws-cdk/lib/context-providers/ssm-parameters.ts b/packages/aws-cdk/lib/context-providers/ssm-parameters.ts new file mode 100644 index 0000000000000..be561fcef3415 --- /dev/null +++ b/packages/aws-cdk/lib/context-providers/ssm-parameters.ts @@ -0,0 +1,28 @@ +import { Mode, SDK } from '../api'; +import { debug } from '../logging'; +import { ContextProviderPlugin } from './provider'; + +/** + * Plugin to read arbitrary SSM parameter names + */ +export class SSMContextProviderPlugin implements ContextProviderPlugin { + constructor(private readonly aws: SDK) { + } + + public async getValue(args: {[key: string]: any}) { + const region = args.region; + const account = args.account; + if (!('parameterName' in args)) { + throw new Error('parameterName must be provided in props for SSMContextProviderPlugin'); + } + const parameterName = args.parameterName; + debug(`Reading SSM parameter ${account}:${region}:${parameterName}`); + + const ssm = await this.aws.ssm(account, region, Mode.ForReading); + const response = await ssm.getParameter({ Name: parameterName }).promise(); + if (!response.Parameter || response.Parameter.Value === undefined) { + throw new Error(`SSM parameter not available in account ${account}, region ${region}: ${parameterName}`); + } + return response.Parameter.Value; + } +} diff --git a/packages/aws-cdk/lib/context-providers/vpcs.ts b/packages/aws-cdk/lib/context-providers/vpcs.ts new file mode 100644 index 0000000000000..daaa5b0ba7662 --- /dev/null +++ b/packages/aws-cdk/lib/context-providers/vpcs.ts @@ -0,0 +1,193 @@ +import cxapi = require('@aws-cdk/cx-api'); +import AWS = require('aws-sdk'); +import { Mode, SDK } from '../api'; +import { debug } from '../logging'; +import { ContextProviderPlugin } from './provider'; + +export class VpcNetworkContextProviderPlugin implements ContextProviderPlugin { + + constructor(private readonly aws: SDK) { + } + + public async getValue(args: cxapi.VpcContextQuery) { + const account: string = args.account; + const region: string = args.region; + + const ec2 = await this.aws.ec2(account, region, Mode.ForReading); + + const vpcId = await this.findVpc(ec2, args); + + return await this.readVpcProps(ec2, vpcId); + } + + private async findVpc(ec2: AWS.EC2, args: {[key: string]: any}): Promise { + const vpcId: string | undefined = args.vpcId; + const vpcName: string | undefined = args.vpcName; + const tags: {[key: string]: string} | undefined = args.tags; + const isDefault: boolean | undefined = args.isDefault; + + // Builter request filter + const filters: AWS.EC2.Filter[] = []; + if (vpcId) { filters.push({ Name: 'vpc-id', Values: [vpcId] }); } + if (vpcName) { filters.push({ Name: 'tag:Name', Values: [vpcName] }); } + if (tags) { + for (const [tag, value] of Object.entries(tags)) { + filters.push({ Name: `tag:${tag}`, Values: [value] }); + } + } + if (isDefault !== undefined) { + filters.push({ Name: 'isDefault', Values: [isDefault ? 'true' : 'false'] }); + } + + debug(`Listing VPCs in ${args.account}:${args.region}`); + const response = await ec2.describeVpcs({ Filters: filters }).promise(); + + const vpcs = response.Vpcs || []; + if (vpcs.length === 0) { + throw new Error(`Could not find any VPCs matching ${JSON.stringify(args)}`); + } + if (vpcs.length > 1) { + throw new Error(`Found ${vpcs.length} VPCs matching ${JSON.stringify(args)}; please narrow the search criteria`); + } + + return vpcs[0].VpcId!; + } + + private async readVpcProps(ec2: AWS.EC2, vpcId: string): Promise { + debug(`Describing VPC ${vpcId}`); + + const response = await ec2.describeSubnets({ Filters: [{ Name: 'vpc-id', Values: [vpcId] }] }).promise(); + const listedSubnets = response.Subnets || []; + + // Now comes our job to separate these subnets out into AZs and subnet groups (Public, Private, Isolated) + // We have the following attributes to go on: + // - Type tag, we tag subnets with their type + // - Name tag, we tag subnets with their subnet group name + // - MapPublicIpOnLaunch in absence of tags => must be a Public subnet, anything else is either Isolated or Private + + const azs = Array.from(new Set(listedSubnets.map(s => s.AvailabilityZone!))); + azs.sort(); + + const subnets: Subnet[] = listedSubnets.map(subnet => { + let type = getTag('aws-cdk:SubnetType', subnet.Tags); + if (type === undefined) { + type = subnet.MapPublicIpOnLaunch ? 'Public' : 'Private'; + } + + const name = getTag('aws-cdk:SubnetName', subnet.Tags) || type; + + return { az: subnet.AvailabilityZone!, type: type as SubnetType, name, subnetId: subnet.SubnetId! }; + }); + + const grouped = groupSubnets(subnets); + + return { + vpcId, + availabilityZones: grouped.azs, + isolatedSubnetIds: collapse(flatMap(findGroups(SubnetType.Isolated, grouped), group => group.subnets.map(s => s.subnetId))), + isolatedSubnetNames: collapse(flatMap(findGroups(SubnetType.Isolated, grouped), group => group.name ? [group.name] : [])), + privateSubnetIds: collapse(flatMap(findGroups(SubnetType.Private, grouped), group => group.subnets.map(s => s.subnetId))), + privateSubnetNames: collapse(flatMap(findGroups(SubnetType.Private, grouped), group => group.name ? [group.name] : [])), + publicSubnetIds: collapse(flatMap(findGroups(SubnetType.Public, grouped), group => group.subnets.map(s => s.subnetId))), + publicSubnetNames: collapse(flatMap(findGroups(SubnetType.Public, grouped), group => group.name ? [group.name] : [])), + }; + } +} + +/** + * Return the value of a tag from a set of tags + */ +function getTag(name: string, tags?: AWS.EC2.Tag[]): string | undefined { + for (const tag of tags || []) { + if (tag.Key === name) { + return tag.Value; + } + } + return undefined; +} + +/** + * Group subnets of the same type together, and order by AZ + */ +function groupSubnets(subnets: Subnet[]): SubnetGroups { + const grouping: {[key: string]: Subnet[]} = {}; + for (const subnet of subnets) { + const key = [subnet.type, subnet.name].toString(); + if (!(key in grouping)) { grouping[key] = []; } + grouping[key].push(subnet); + } + + const groups = Object.values(grouping).map(sns => { + sns.sort((a: Subnet, b: Subnet) => a.az.localeCompare(b.az)); + return { + type: sns[0].type, + name: sns[0].name, + subnets: sns, + }; + }); + + const azs = groups[0].subnets.map(s => s.az); + + for (const group of groups) { + const groupAZs = group.subnets.map(s => s.az); + if (!arraysEqual(groupAZs, azs)) { + throw new Error(`Not all subnets in VPC have the same AZs: ${groupAZs} vs ${azs}`); + } + } + + return { azs, groups }; +} + +enum SubnetType { + Public = 'Public', + Private = 'Private', + Isolated = 'Isolated' +} + +interface Subnet { + az: string; + type: SubnetType; + name?: string; + subnetId: string; +} + +interface SubnetGroup { + type: SubnetType; + name?: string; + subnets: Subnet[]; +} + +interface SubnetGroups { + azs: string[]; + groups: SubnetGroup[]; +} + +function arraysEqual(as: string[], bs: string[]): boolean { + if (as.length !== bs.length) { return false; } + + for (let i = 0; i < as.length; i++) { + if (as[i] !== bs[i]) { + return false; + } + } + + return true; +} + +function findGroups(type: SubnetType, groups: SubnetGroups): SubnetGroup[] { + return groups.groups.filter(g => g.type === type); +} + +function flatMap(xs: T[], fn: (x: T) => U[]): U[] { + const ret = new Array(); + for (const x of xs) { + ret.push(...fn(x)); + } + return ret; +} + +function collapse(xs: T[]): T[] | undefined { + if (xs.length > 0) { return xs; } + return undefined; + +} \ No newline at end of file diff --git a/tools/cdk-integ-tools/lib/integ-helpers.ts b/tools/cdk-integ-tools/lib/integ-helpers.ts index 60936c394a5c6..e0a40d3ca9ab3 100644 --- a/tools/cdk-integ-tools/lib/integ-helpers.ts +++ b/tools/cdk-integ-tools/lib/integ-helpers.ts @@ -115,6 +115,26 @@ export const STATIC_TEST_CONTEXT = { "ssm:account=12345678:parameterName=/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2:region=test-region": "ami-1234", "ssm:account=12345678:parameterName=/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2:region=test-region": "ami-1234", "ssm:account=12345678:parameterName=/aws/service/ecs/optimized-ami/amazon-linux/recommended:region=test-region": "{\"image_id\": \"ami-1234\"}", + "vpc-provider:account=12345678:isDefault=true:region=test-region": { + "vpcId": "vpc-60900905", + "availabilityZones": [ + "us-east-1a", + "us-east-1b", + "us-east-1c" + ], + "isolatedSubnetIds": [], + "isolatedSubnetNames": [], + "privateSubnetIds": [], + "privateSubnetNames": [], + "publicSubnetIds": [ + "subnet-e19455ca", + "subnet-e0c24797", + "subnet-ccd77395", + ], + "publicSubnetNames": [ + "Public" + ] + } }; /**