diff --git a/packages/aws-cdk-ec2/.gitignore b/packages/aws-cdk-ec2/.gitignore new file mode 100644 index 0000000000000..26bed542eb091 --- /dev/null +++ b/packages/aws-cdk-ec2/.gitignore @@ -0,0 +1,6 @@ +*.js +tsconfig.json +*.js.map +*.d.ts +dist +test/cdk.json diff --git a/packages/aws-cdk-ec2/README.md b/packages/aws-cdk-ec2/README.md new file mode 100644 index 0000000000000..e817d6fb82240 --- /dev/null +++ b/packages/aws-cdk-ec2/README.md @@ -0,0 +1,182 @@ +## AWS Compute and Networking Construct Library + +The `aws-cdk-ec2` package contains primitives for setting up networking, +instances, and load balancers. + +### VPC + +Most projects need a Virtual Private Cloud to provide security by means of +network partitioning. This is easily achieved by creating an instance of +`VpcNetwork`: + +```ts +import { VpcNetwork } from 'aws-cdk-ec2'; + +const vpc = new VpcNetwork(this, 'VPC'); +``` + +All default Constructs requires EC2 instances to be launched inside a VPC, so +you should generally start by defining a VPC whenever you need to launch +instances for your project. + +Our default `VpcNetwork` class creates a private and public subnet for every +availability zone. Classes that use the VPC will generally launch instances +into all private subnets, and provide a parameter called `vpcPlacement` to +allow you to override the placement. [Read more about +subnets](https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html). + +### Fleet + +A `Fleet` represents a number of instances on which you run your code. You +pick the size of the fleet, the instance type and the OS image: + +```ts +import { Fleet, InstanceClass, InstanceSize, InstanceTypePair, makeLinuxMachineImage, VpcNetwork } from '../lib'; + +new Fleet(stack, 'Fleet', { + vpc, + instanceType: new InstanceTypePair(InstanceClass.Burstable2, InstanceSize.Micro), + machineImage: new LinuxImage({ + 'us-east-1': 'ami-97785bed' + }) +}); +``` + +> NOTE: Fleet has an property called `allowAllOutbound` (allowing the instances to contact the +> internet) which is set to `true` by default. Be sure to set this to `false` if you don't want +> your instances to be able to start arbitrary connections. + +### AMIs + +AMIs control the OS that gets launched when you start your instance. + +Depending on the type of AMI, you select it a different way. + +The latest version of Windows images are regionally published under labels, +so you can select Windows images like this: + + new WindowsImage(WindowsVersion.WindowsServer2016EnglishNanoBase) + +You can select the latest Amazon Linux image like this: + + new AmazonLinuxImage() + +Other Linux images are unfortunately not currently published this way, so you have +to supply a region-to-AMI map when creating a Linux image: + + machineImage: new GenericLinuxImage({ + 'us-east-1': 'ami-97785bed', + 'eu-west-1': 'ami-12345678', + // ... + }) + +> NOTE: Selecting Linux images will change when the information is published in an automatically +> consumable way. + +### Load Balancer + +Load balancers send traffic to one or more fleets. Create a load balancer, +set up listeners and a health check, and supply the fleet(s) you want to load +balance to in the `targets` property. + +The load balancer allows all connections by default. If you want to change that, +pass the `allowConnectionsFrom` property while setting up the listener. + +```ts +new ClassicLoadBalancer(stack, 'LB', { + vpc, + internetFacing: true, + listeners: [{ + externalPort: 80, + }], + healthCheck: { + port: 80 + }, + targets: [fleet] +}); +``` + +### Allowing Connections + +In AWS, all connections to and from EC2 instances are governed by *Security +Groups*. You can think of these as a firewall with rules. All Constructs that +create instances on your behalf implicitly have such a security group. +Unless otherwise indicated using properites, the security groups start out +empty; that is, no connections are allowed by default. + +In general, whenever you link two Constructs together (such as the load balancer and the +fleet in the previous example), the security groups will be automatically updated to allow +network connections between the indicated instances. In other cases, you will need to +configure these allows connections yourself, for example if the connections you want to +allow do not originate from instances in a CDK construct, or if you want to allow +connections among instances inside a single security group. + +All Constructs with security groups have a member called `connections`, which +can be used to configure permissible connections. In the most general case, a +call to allow connections needs both a connection peer and the type of +connection to allow: + +```ts +lb.connections.allowFrom(new AnyIPv4(), new TcpPort(443), 'Allow inbound'); + +// Or using a convenience function +lb.connections.allowFromAnyIpv4(new TcpPort(443), 'Allow inbound'); +``` + +### Connection Peers + +There are various classes that implement the connection peer part: + +```ts +// Simple connection peers +let peer = new CidrIp("10.0.0.0/16"); +let peer = new AnyIPv4(); +let peer = new CidrIpv6("::0/0"); +let peer = new AnyIPv6(); +let peer = new PrefixList("pl-12345"); +fleet.connections.allowTo(peer, new TcpPort(443), 'Allow outbound HTTPS'); +``` + +Any object that has a security group can itself be used as a connection peer: + +```ts +// These automatically create appropriate ingress and egress rules in both security groups +fleet1.connections.allowTo(fleet2, new TcpPort(80), 'Allow between fleets'); + +fleet.connections.allowTcpPort(80), 'Allow from load balancer'); +``` + +### Port Ranges + +The connections that are allowed are specified by port ranges. A number of classes provide +the connection specifier: + +```ts +new TcpPort(80); +new TcpPortRange(60000, 65535); +new TcpAllPorts(); +new AllConnections(); +``` + +> NOTE: This set is not complete yet; for example, there is no library support for ICMP at the moment. +> However, you can write your own classes to implement those. + +### Default Ports + +Some Constructs have default ports associated with them. For example, the +listener of a load balancer does (it's the public port), or instances of an +RDS database (it's the port the database is accepting connections on). + +If the object you're calling the peering method on has a default port associated with it, you can call +`allowDefaultPortFrom()` and omit the port specifier. If the argument has an associated default port, call +`allowToDefaultPort()`. + +For example: + +```ts +// Port implicit in listener +listener.connections.allowDefaultPortFromAnyIpv4('Allow public'); + +// Port implicit in peer +fleet.connections.allowToDefaultPort(rdsDatabase, 'Fleet can access database'); +``` diff --git a/packages/aws-cdk-ec2/lib/connection.ts b/packages/aws-cdk-ec2/lib/connection.ts new file mode 100644 index 0000000000000..bee8817fc98b8 --- /dev/null +++ b/packages/aws-cdk-ec2/lib/connection.ts @@ -0,0 +1,223 @@ +import { Token } from "aws-cdk"; +import { IConnectable, IConnections, SecurityGrouplessConnections } from "./connections"; + +/** + * Interface for classes that provide the peer-specification parts of a security group rule + */ +export interface IConnectionPeer { + /** + * Whether the rule can be inlined into a SecurityGroup or not + */ + readonly canInlineRule: boolean; + + /** + * Produce the ingress rule JSON for the given connection + */ + toIngressRuleJSON(): any; + + /** + * Produce the egress rule JSON for the given connection + */ + toEgressRuleJSON(): any; +} + +/** + * A connection to and from a given IP range + */ +export class CidrIp implements IConnectionPeer, IConnectable { + public readonly canInlineRule = true; + public readonly connections: IConnections; + + constructor(private readonly cidrIp: string) { + this.connections = new SecurityGrouplessConnections(this); + } + + /** + * Produce the ingress rule JSON for the given connection + */ + public toIngressRuleJSON(): any { + return { cidrIp: this.cidrIp }; + } + /** + * Produce the egress rule JSON for the given connection + */ + public toEgressRuleJSON(): any { + return { cidrIp: this.cidrIp }; + } +} + +/** + * Any IPv4 address + */ +export class AnyIPv4 extends CidrIp { + constructor() { + super("0.0.0.0/0"); + } +} + +/** + * A connection to a from a given IPv6 range + */ +export class CidrIpv6 implements IConnectionPeer, IConnectable { + public readonly canInlineRule = true; + public readonly connections: IConnections; + + constructor(private readonly cidrIpv6: string) { + this.connections = new SecurityGrouplessConnections(this); + } + + /** + * Produce the ingress rule JSON for the given connection + */ + public toIngressRuleJSON(): any { + return { cidrIpv6: this.cidrIpv6 }; + } + /** + * Produce the egress rule JSON for the given connection + */ + public toEgressRuleJSON(): any { + return { cidrIpv6: this.cidrIpv6 }; + } +} + +/** + * Any IPv6 address + */ +export class AnyIPv6 extends CidrIpv6 { + constructor() { + super("::0/0"); + } +} + +/** + * A prefix list + * + * Prefix lists are used to allow traffic to VPC-local service endpoints. + * + * For more information, see this page: + * + * https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-endpoints.html + */ +export class PrefixList implements IConnectionPeer, IConnectable { + public readonly canInlineRule = true; + public readonly connections: IConnections; + + constructor(private readonly prefixListId: string) { + this.connections = new SecurityGrouplessConnections(this); + } + + public toIngressRuleJSON(): any { + throw new Error('Prefix lists can only be used for egress rules'); + } + + public toEgressRuleJSON(): any { + return { destinationPrefixListId: this.prefixListId }; + } +} + +/** + * Interface for classes that provide the connection-specification parts of a security group rule + */ +export interface IPortRange { + readonly canInlineRule: boolean; + + /** + * Produce the ingress/egress rule JSON for the given connection + */ + toRuleJSON(): any; +} + +/** + * Protocol for use in Connection Rules + */ +export enum Protocol { + All = '-1', + Tcp = 'tcp', + Udp = 'udp', + Icmp = 'icmp', + Icmpv6 = '58', +} + +/** + * A single TCP port + */ +export class TcpPort implements IPortRange { + public readonly canInlineRule = true; + + constructor(private readonly port: number) { + } + + public toRuleJSON(): any { + return { + ipProtocol: Protocol.Tcp, + fromPort: this.port, + toPort: this.port + }; + } +} + +/** + * A single TCP port that is provided by a resource attribute + */ +export class TcpPortFromAttribute implements IPortRange { + public readonly canInlineRule = false; + + constructor(private readonly port: Token) { + } + + public toRuleJSON(): any { + return { + ipProtocol: Protocol.Tcp, + fromPort: this.port, + toPort: this.port + }; + } +} + +/** + * A TCP port range + */ +export class TcpPortRange implements IPortRange { + public readonly canInlineRule = true; + + constructor(private readonly startPort: number, private readonly endPort: number) { + } + + public toRuleJSON(): any { + return { + ipProtocol: Protocol.Tcp, + fromPort: this.startPort, + toPort: this.endPort + }; + } +} + +/** + * All TCP Ports + */ +export class TcpAllPorts implements IPortRange { + public readonly canInlineRule = true; + + public toRuleJSON(): any { + return { + ipProtocol: Protocol.Tcp, + fromPort: 0, + toPort: 65535 + }; + } +} + +/** + * All TCP Ports + */ +export class AllConnections implements IPortRange { + public readonly canInlineRule = true; + + public toRuleJSON(): any { + return { + ipProtocol: '-1', + fromPort: -1, + toPort: -1, + }; + } +} \ No newline at end of file diff --git a/packages/aws-cdk-ec2/lib/connections.ts b/packages/aws-cdk-ec2/lib/connections.ts new file mode 100644 index 0000000000000..2bd27e1a13e7a --- /dev/null +++ b/packages/aws-cdk-ec2/lib/connections.ts @@ -0,0 +1,195 @@ +import { AnyIPv4, IConnectionPeer, IPortRange } from "./connection"; +import { ISecurityGroup } from "./security-group"; + +/** + * The goal of this module is to make possible to write statements like this: + * + * ```ts + * database.connections.allowFrom(fleet); + * fleet.connections.allowTo(database); + * rdgw.connections.allowFromCidrIp('0.3.1.5/86'); + * rgdw.connections.allowTrafficTo(fleet, new AllPorts()); + * ``` + * + * The insight here is that some connecting peers have information on what ports should + * be involved in the connection, and some don't. + * + * Constructs will make their `connections` property to be equal to an instance of + * either `Connections` or `ConnectionsWithDefault`. + */ + +/** + * An object that has a Connections object + */ +export interface IConnectable { + readonly connections: IConnections; +} + +/** + * An object that has a Connections object as well as a default port range. + */ +export interface IDefaultConnectable extends IConnectable { + readonly defaultPortRange: IPortRange; +} + +/** + * An object that encapsulates connection logic + * + * The IConnections object both has knowledge on what peer to use, + * as well as how to add connection rules. + */ +export interface IConnections { + /** + * Access to the peer that we're connecting to + * + * It's convenient to put this on the Connections object since + * all participants in this protocol have one anyway, and the Connections + * objects have access to it, so they don't need to implement two interfaces. + */ + readonly connectionPeer: IConnectionPeer; + + /** + * Allow connections to the peer on the given port + */ + allowTo(other: IConnectable, portRange: IPortRange, description: string): void; + + /** + * Allow connections from the peer on the given port + */ + allowFrom(other: IConnectable, portRange: IPortRange, description: string): void; +} + +/** + * Connections for an object that does not have default ports + */ +export class Connections implements IConnections { + public readonly connectionPeer: IConnectionPeer; + + constructor(private readonly securityGroup: ISecurityGroup) { + this.connectionPeer = this.securityGroup; + } + + /** + * Allow connections to the peer on their default port + */ + public allowToDefaultPort(other: IDefaultConnectable, description: string) { + this.allowTo(other, other.defaultPortRange, description); + } + + /** + * Allow connections to the peer on the given port + */ + public allowTo(other: IConnectable, portRange: IPortRange, description: string) { + this.securityGroup.addEgressRule(other.connections.connectionPeer, portRange, description); + other.connections.allowFrom( + new ConnectionsHolder(new SecurityGrouplessConnections(this.connectionPeer)), + portRange, + description); + } + + /** + * Allow connections from the peer on the given port + */ + public allowFrom(other: IConnectable, portRange: IPortRange, description: string) { + this.securityGroup.addIngressRule(other.connections.connectionPeer, portRange, description); + other.connections.allowTo( + new ConnectionsHolder(new SecurityGrouplessConnections(this.connectionPeer)), + portRange, + description); + } + + /** + * Allow hosts inside the security group to connect to each other on the given port + */ + public allowInternally(portRange: IPortRange, description: string) { + this.securityGroup.addIngressRule(this.securityGroup, portRange, description); + } + + /** + * Allow to all IPv4 ranges + */ + public allowToAnyIpv4(portRange: IPortRange, description: string) { + this.allowTo(new AnyIPv4(), portRange, description); + } + + /** + * Allow from any IPv4 ranges + */ + public allowFromAnyIpv4(portRange: IPortRange, description: string) { + this.allowFrom(new AnyIPv4(), portRange, description); + } +} + +/** + * A class to orchestrate connections that already has default ports + */ +export class DefaultConnections extends Connections { + public readonly defaultPortRange: IPortRange; + + constructor(securityGroup: ISecurityGroup, defaultPortRangeProvider: IDefaultConnectable) { + // We take a IDefaultConnectable as an argument instead of the port + // range directly so (a) we force the containing construct to implement + // IDefaultConnectable and then (b) so they don't have to repeat the information. + // + // Slightly risky since this requires that the container initializes in the right order. + super(securityGroup); + this.defaultPortRange = defaultPortRangeProvider.defaultPortRange; + + if (this.defaultPortRange == null) { + throw new Error("Ordering problem: create DefaultConnections() after initializing defaultPortRange"); + } + } + + /** + * Allow connections from the peer on our default port + * + * Even if the peer has a default port, we will always use our default port. + */ + public allowDefaultPortFrom(other: IConnectable, description: string) { + this.allowFrom(other, this.defaultPortRange, description); + } + + /** + * Allow hosts inside the security group to connect to each other + */ + public allowDefaultPortInternally(description: string) { + this.allowInternally(this.defaultPortRange, description); + } + + /** + * Allow default connections from all IPv4 ranges + */ + public allowDefaultPortFromAnyIpv4(description: string) { + this.allowFromAnyIpv4(this.defaultPortRange, description); + } +} + +/** + * This object is used by peers who don't allow reverse connections + * + * It still has an associated connection peer, but that peer does not + * have any security groups to add connections to. + */ +export class SecurityGrouplessConnections implements IConnections { + constructor(public readonly connectionPeer: IConnectionPeer) { + } + + public allowTo(_other: IConnectable, _connection: IPortRange, _description: string): void { + // Nothing to do + } + + public allowFrom(_other: IConnectable, _connection: IPortRange, _description: string): void { + // Nothing to do + } +} + +/** + * Class that implements IConnectable that can be constructed + * + * This is simply used to implement IConnectable when we need + * to make reverse connections. + */ +class ConnectionsHolder implements IConnectable { + constructor(public readonly connections: IConnections) { + } +} diff --git a/packages/aws-cdk-ec2/lib/fleet.ts b/packages/aws-cdk-ec2/lib/fleet.ts new file mode 100644 index 0000000000000..f247ba94929de --- /dev/null +++ b/packages/aws-cdk-ec2/lib/fleet.ts @@ -0,0 +1,196 @@ +import { Construct, FnBase64, PolicyStatement, ServicePrincipal, Token } from 'aws-cdk'; +import { Role } from 'aws-cdk-iam'; +import { autoscaling, iam, sns } from 'aws-cdk-resources'; +import { AllConnections, AnyIPv4, IConnectionPeer } from './connection'; +import { Connections } from './connections'; +import { InstanceType } from './instance-types'; +import { ClassicLoadBalancer, IClassicLoadBalancerTarget } from './load-balancer'; +import { IMachineImageSource, OperatingSystemType } from './machine-image'; +import { SecurityGroup } from './security-group'; +import { VpcNetworkRef, VpcPlacementStrategy } from './vpc-ref'; + +/** + * Properties of a Fleet + */ +export interface FleetProps { + /** + * Type of instance to launch + */ + instanceType: InstanceType; + + /** + * Minimum number of instances in the fleet + * @default 1 + */ + minSize?: number; + + /** + * Maximum number of instances in the fleet + * @default 1 + */ + maxSize?: number; + + /** + * Initial amount of instances in the fleet + * @default 1 + */ + desiredCapacity?: number; + + /** + * Name of SSH keypair to grant access to instances + * @default No SSH access will be possible + */ + keyName?: string; + + /** + * AMI to launch + */ + machineImage: IMachineImageSource; + + /** + * VPC to launch these instances in. + */ + vpc: VpcNetworkRef; + + /** + * Where to place instances within the VPC + */ + vpcPlacement?: VpcPlacementStrategy; + + /** + * SNS topic to send notifications about fleet changes + * @default No fleet change notifications will be sent. + */ + notificationsTopic?: sns.TopicResource; + + /** + * Whether the instances can initiate connections to anywhere by default + * + * @default true + */ + allowAllOutbound?: boolean; +} + +/** + * A Fleet represents a managed set of EC2 instances + * + * The Fleet models a number of AutoScalingGroups, a launch configuration, a + * security group and an instance role. + * + * It allows adding arbitrary commands to the startup scripts of the instances + * in the fleet. + * + * The ASG spans all availability zones. + */ +export class Fleet extends Construct implements IClassicLoadBalancerTarget { + public readonly connectionPeer: IConnectionPeer; + + /** + * The type of OS instances of this fleet are running. + */ + public readonly osType: OperatingSystemType; + + /** + * Allows specify security group connections for instances of this fleet. + */ + public readonly connections: Connections; + + /** + * The IAM role assumed by instances of this fleet. + */ + public readonly role: Role; + + private readonly userDataLines = new Array(); + private readonly autoScalingGroup: autoscaling.AutoScalingGroupResource; + private readonly securityGroup: SecurityGroup; + private readonly loadBalancerNames: Token[] = []; + + constructor(parent: Construct, name: string, props: FleetProps) { + super(parent, name); + + this.securityGroup = new SecurityGroup(this, 'InstanceSecurityGroup', { vpc: props.vpc }); + this.connections = new Connections(this.securityGroup); + this.connectionPeer = this.securityGroup; + + if (props.allowAllOutbound !== false) { + this.connections.allowTo(new AnyIPv4(), new AllConnections(), 'Outbound traffic allowed by default'); + } + + this.role = new Role(this, 'InstanceRole', { + assumedBy: new ServicePrincipal('ec2.amazonaws.com') + }); + + const iamProfile = new iam.InstanceProfileResource(this, 'InstanceProfile', { + roles: [ this.role.roleName ] + }); + + // use delayed evaluation + const machineImage = props.machineImage.getImage(this); + const userDataToken = new Token(() => new FnBase64((machineImage.os.createUserData(this.userDataLines)))); + + const launchConfig = new autoscaling.LaunchConfigurationResource(this, 'LaunchConfig', { + imageId: machineImage.imageId, + keyName: props.keyName, + instanceType: props.instanceType.toString(), + securityGroups: [this.securityGroup.securityGroupId], + iamInstanceProfile: iamProfile.ref, + userData: userDataToken + }); + + launchConfig.addDependency(this.role); + + const minSize = props.minSize || 1; + const maxSize = props.maxSize || 1; + const desiredCapacity = props.desiredCapacity || 1; + + const asgProps: autoscaling.AutoScalingGroupResourceProps = { + minSize: minSize.toString(), + maxSize: maxSize.toString(), + desiredCapacity: desiredCapacity.toString(), + launchConfigurationName: launchConfig.ref, + loadBalancerNames: new Token(() => this.loadBalancerNames), + }; + + if (props.notificationsTopic) { + asgProps.notificationConfigurations = []; + asgProps.notificationConfigurations.push({ + topicArn: props.notificationsTopic.ref, + notificationTypes: [ + "autoscaling:EC2_INSTANCE_LAUNCH", + "autoscaling:EC2_INSTANCE_LAUNCH_ERROR", + "autoscaling:EC2_INSTANCE_TERMINATE", + "autoscaling:EC2_INSTANCE_TERMINATE_ERROR" + ], + }); + } + + const subnets = props.vpc.subnets(props.vpcPlacement); + asgProps.vpcZoneIdentifier = subnets.map(n => n.subnetId); + + this.autoScalingGroup = new autoscaling.AutoScalingGroupResource(this, 'ASG', asgProps); + this.osType = machineImage.os.type; + } + + public attachToClassicLB(loadBalancer: ClassicLoadBalancer): void { + this.loadBalancerNames.push(loadBalancer.loadBalancerName); + } + + /** + * Add command to the startup script of fleet instances. + * The command must be in the scripting language supported by the fleet's OS (i.e. Linux/Windows). + */ + public addUserData(script: string) { + this.userDataLines.push(script); + } + + public autoScalingGroupName() { + return this.autoScalingGroup.ref; + } + + /** + * Adds a statement to the IAM role assumed by instances of this fleet. + */ + public addToRolePolicy(statement: PolicyStatement) { + this.role.addToPolicy(statement); + } +} diff --git a/packages/aws-cdk-ec2/lib/index.ts b/packages/aws-cdk-ec2/lib/index.ts new file mode 100644 index 0000000000000..efda2fcff5187 --- /dev/null +++ b/packages/aws-cdk-ec2/lib/index.ts @@ -0,0 +1,9 @@ +export * from './connection'; +export * from './connections'; +export * from './fleet'; +export * from './instance-types'; +export * from './load-balancer'; +export * from './machine-image'; +export * from './security-group'; +export * from './vpc'; +export * from './vpc-ref'; diff --git a/packages/aws-cdk-ec2/lib/instance-types.ts b/packages/aws-cdk-ec2/lib/instance-types.ts new file mode 100644 index 0000000000000..4058e0bf2dfb3 --- /dev/null +++ b/packages/aws-cdk-ec2/lib/instance-types.ts @@ -0,0 +1,244 @@ +/** + * What class and generation of instance to use + * + * We have both symbolic and concrete enums for every type. + * + * The first are for people that want to specify by purpose, + * the second one are for people who already know exactly what + * 'R4' means. + */ +export enum InstanceClass { + /** + * Standard instances, 3rd generation + */ + Standard3 = 'm3', + + /** + * Standard instances, 3rd generation + */ + M3 = 'm3', + + /** + * Standard instances, 4th generation + */ + Standard4 = 'm4', + + /** + * Standard instances, 4th generation + */ + M4 = 'm4', + + /** + * Standard instances, 5th generation + */ + Standard5 = 'm5', + + /** + * Standard instances, 5th generation + */ + M5 = 'm5', + + /** + * Memory optimized instances, 3rd generation + */ + Memory3 = 'r3', + + /** + * Memory optimized instances, 3rd generation + */ + R3 = 'r3', + + /** + * Memory optimized instances, 3rd generation + */ + Memory4 = 'r4', + + /** + * Memory optimized instances, 3rd generation + */ + R4 = 'r4', + + /** + * Compute optimized instances, 3rd generation + */ + Compute3 = 'c3', + + /** + * Compute optimized instances, 3rd generation + */ + C3 = 'c3', + + /** + * Compute optimized instances, 4th generation + */ + Compute4 = 'c4', + + /** + * Compute optimized instances, 4th generation + */ + C4 = 'c4', + + /** + * Compute optimized instances, 5th generation + */ + Compute5 = 'c5', + + /** + * Compute optimized instances, 5th generation + */ + C5 = 'c5', + + /** + * Storage-optimized instances, 2nd generation + */ + Storage2 = 'd2', + + /** + * Storage-optimized instances, 2nd generation + */ + D2 = 'd2', + + /** + * Storage/compute balanced instances, 1st generation + */ + StorageCompute1 = 'h1', + + /** + * Storage/compute balanced instances, 1st generation + */ + H1 = 'h1', + + /** + * I/O-optimized instances, 3rd generation + */ + Io3 = 'i3', + + /** + * I/O-optimized instances, 3rd generation + */ + I3 = 'i3', + + /** + * Burstable instances, 2nd generation + */ + Burstable2 = 't2', + + /** + * Burstable instances, 2nd generation + */ + T2 = 't2', + + /** + * Memory-intensive instances, 1st generation + */ + MemoryIntensive1 = 'x1', + + /** + * Memory-intensive instances, 1st generation + */ + X1 = 'x1', + + /** + * Memory-intensive instances, extended, 1st generation + */ + MemoryIntensive1Extended = 'x1e', + + /** + * Memory-intensive instances, 1st generation + */ + X1e = 'x1e', + + /** + * Instances with customizable hardware acceleration, 1st generation + */ + Fpga1 = 'f1', + + /** + * Instances with customizable hardware acceleration, 1st generation + */ + F1 = 'f1', + + /** + * Graphics-optimized instances, 3rd generation + */ + Graphics3 = 'g3', + + /** + * Graphics-optimized instances, 3rd generation + */ + G3 = 'g3', + + /** + * Parallel-processing optimized instances, 2nd generation + */ + Parallel2 = 'p2', + + /** + * Parallel-processing optimized instances, 2nd generation + */ + P2 = 'p2', + + /** + * Parallel-processing optimized instances, 3nd generation + */ + Parallel3 = 'p3', + + /** + * Parallel-processing optimized instances, 3nd generation + */ + P3 = 'p3', +} + +/** + * What size of instance to use + */ +export enum InstanceSize { + Micro = 'micro', + Small = 'small', + Medium = 'medium', + Large = 'large', + XLarge = 'xlarge', + XLarge2 = '2xlarge', + XLarge4 = '4xlarge', + XLarge8 = '8xlarge', + XLarge9 = '9xlarge', + XLarge10 = '10xlarge', + XLarge12 = '12xlarge', + XLarge16 = '16xlarge', + XLarge18 = '18xlarge', + XLarge24 = '24xlarge', + XLarge32 = '32xlarge', +} + +/** + * Instance type for EC2 instances + * + * This class takes a literal string, good if you already + * know the identifier of the type you want. + */ +export class InstanceType { + constructor(private readonly instanceTypeIdentifier: string) { + } + + /** + * Return the instance type as a dotted string + */ + public toString(): string { + return this.instanceTypeIdentifier; + } +} + +/** + * Instance type for EC2 instances + * + * This class takes a combination of a class and size. + * + * Be aware that not all combinations of class and size are available, and not all + * classes are available in all regions. + */ +export class InstanceTypePair extends InstanceType { + constructor(public readonly instanceClass: InstanceClass, + public readonly instanceSize: InstanceSize) { + super(instanceClass + '.' + instanceSize); + } +} diff --git a/packages/aws-cdk-ec2/lib/load-balancer.ts b/packages/aws-cdk-ec2/lib/load-balancer.ts new file mode 100644 index 0000000000000..592f8fbcb4f16 --- /dev/null +++ b/packages/aws-cdk-ec2/lib/load-balancer.ts @@ -0,0 +1,385 @@ +import { Arn, Construct, Token } from 'aws-cdk'; +import { elasticloadbalancing } from 'aws-cdk-resources'; +import { AnyIPv4, IConnectionPeer, IPortRange, TcpPort } from './connection'; +import { Connections, DefaultConnections, IConnectable, IDefaultConnectable } from './connections'; +import { ISecurityGroup, SecurityGroup } from './security-group'; +import { VpcNetworkRef, VpcSubnetRef } from './vpc-ref'; + +/** + * Construction properties for a ClassicLoadBalancer + */ +export interface ClassicLoadBalancerProps { + /** + * VPC network of the fleet instances + */ + vpc: VpcNetworkRef; + + /** + * Whether this is an internet-facing Load Balancer + * + * This controls whether the LB has a public IP address assigned. It does + * not open up the Load Balancer's security groups to public internet access. + * + * @default false + */ + internetFacing?: boolean; + + /** + * What listeners to set up for the load balancer. + * + * Can also be added by .addListener() + */ + listeners?: ClassicLoadBalancerListener[]; + + /** + * What targets to load balance to. + * + * Can also be added by .addTarget() + */ + targets?: IClassicLoadBalancerTarget[]; + + /** + * Health check settings for the load balancing targets. + * + * Not required but recommended. + */ + healthCheck?: HealthCheck; +} + +/** + * Describe the health check to a load balancer + */ +export interface HealthCheck { + /** + * What port number to health check on + */ + port: number; + + /** + * What protocol to use for health checking + * + * The protocol is automatically determined from the port if it's not supplied. + * + * @default Automatic + */ + protocol?: LoadBalancingProtocol; + + /** + * What path to use for HTTP or HTTPS health check (must return 200) + * + * For SSL and TCP health checks, accepting connections is enough to be considered + * healthy. + * + * @default "/" + */ + path?: string; + + /** + * After how many successful checks is an instance considered healthy + * + * @default 2 + */ + healthyThreshold?: number; + + /** + * After how many unsuccessful checks is an instance considered unhealthy + * + * @default 5 + */ + unhealthyThreshold?: number; + + /** + * Number of seconds between health checks + * + * @default 30 + */ + interval?: number; + + /** + * Health check timeout + * + * @default 5 + */ + timeout?: number; +} + +/** + * Interface that is going to be implemented by constructs that you can load balance to + */ +export interface IClassicLoadBalancerTarget extends IConnectable { + /** + * Attach load-balanced target to a classic ELB + */ + attachToClassicLB(loadBalancer: ClassicLoadBalancer): void; +} + +/** + * Add a backend to the load balancer + */ +export interface ClassicLoadBalancerListener { + /** + * External listening port + */ + externalPort: number; + + /** + * What public protocol to use for load balancing + * + * Either 'tcp', 'ssl', 'http' or 'https'. + * + * May be omitted if the external port is either 80 or 443. + */ + externalProtocol?: LoadBalancingProtocol; + + /** + * Instance listening port + * + * Same as the externalPort if not specified. + * + * @default externalPort + */ + internalPort?: number; + + /** + * What public protocol to use for load balancing + * + * Either 'tcp', 'ssl', 'http' or 'https'. + * + * May be omitted if the internal port is either 80 or 443. + * + * The instance protocol is 'tcp' if the front-end protocol + * is 'tcp' or 'ssl', the instance protocol is 'http' if the + * front-end protocol is 'https'. + */ + internalProtocol?: LoadBalancingProtocol; + + /** + * SSL policy names + */ + policyNames?: string[]; + + /** + * ID of SSL certificate + */ + sslCertificateId?: Arn; + + /** + * Allow connections to the load balancer from the given set of connection peers + * + * By default, connections will be allowed from anywhere. Set this to an empty list + * to deny connections, or supply a custom list of peers to allow connections from + * (IP ranges or security groups). + * + * @default Anywhere + */ + allowConnectionsFrom?: IConnectable[]; +} + +export enum LoadBalancingProtocol { + Tcp = 'tcp', + Ssl = 'ssl', + Http = 'http', + Https = 'https' +} + +/** + * A load balancer with a single listener + * + * Routes to a fleet of of instances in a VPC. + */ +export class ClassicLoadBalancer extends Construct implements IConnectable { + /** + * Control all connections from and to this load balancer + */ + public readonly connections: Connections; + + public readonly connectionPeer: IConnectionPeer; + + /** + * An object controlling specifically the connections for each listener added to this load balancer + */ + public readonly listenerPorts: ClassicListenerPort[] = []; + + private readonly elb: elasticloadbalancing.LoadBalancerResource; + private readonly securityGroup: SecurityGroup; + private readonly listeners: elasticloadbalancing.LoadBalancerResource.ListenersProperty[] = []; + + private readonly instancePorts: number[] = []; + private readonly targets: IClassicLoadBalancerTarget[] = []; + + constructor(parent: Construct, name: string, props: ClassicLoadBalancerProps) { + super(parent, name); + + this.securityGroup = new SecurityGroup(this, 'SecurityGroup', { vpc: props.vpc }); + this.connections = new Connections(this.securityGroup); + this.connectionPeer = this.securityGroup; + + // Depending on whether the ELB has public or internal IPs, pick the right backend subnets + const subnets: VpcSubnetRef[] = props.internetFacing ? props.vpc.publicSubnets : props.vpc.privateSubnets; + + this.elb = new elasticloadbalancing.LoadBalancerResource(this, 'Resource', { + securityGroups: [ this.securityGroup.securityGroupId ], + subnets: subnets.map(s => s.subnetId), + listeners: new Token(() => this.listeners), + scheme: props.internetFacing ? 'internet-facing' : 'internal', + healthCheck: props.healthCheck && healthCheckToJSON(props.healthCheck), + }); + + ifUndefined(props.listeners, []).forEach(b => this.addListener(b)); + ifUndefined(props.targets, []).forEach(t => this.addTarget(t)); + } + + /** + * Add a backend to the load balancer + * + * @returns A ClassicListenerPort object that controls connections to the listener port + */ + public addListener(listener: ClassicLoadBalancerListener): ClassicListenerPort { + const protocol = ifUndefinedLazy(listener.externalProtocol, () => wellKnownProtocol(listener.externalPort)); + const instancePort = listener.internalPort || listener.externalPort; + const instanceProtocol = ifUndefined(listener.internalProtocol, + ifUndefined(tryWellKnownProtocol(instancePort), + isHttpProtocol(protocol) ? LoadBalancingProtocol.Http : LoadBalancingProtocol.Tcp)); + + this.listeners.push({ + loadBalancerPort: listener.externalPort.toString(), + protocol, + instancePort: instancePort.toString(), + instanceProtocol, + sslCertificateId: listener.sslCertificateId, + policyNames: listener.policyNames + }); + + const port = new ClassicListenerPort(this.securityGroup, new TcpPort(listener.externalPort)); + + // Allow connections on the public port for all supplied peers (default: everyone) + ifUndefined(listener.allowConnectionsFrom, [new AnyIPv4()]).forEach(peer => { + port.connections.allowDefaultPortFrom(peer, `Default rule allow on ${listener.externalPort}`); + }); + + this.newInstancePort(instancePort); + + // Keep track using array so user can get to them even if they were all supplied in the constructor + this.listenerPorts.push(port); + + return port; + } + + public addTarget(target: IClassicLoadBalancerTarget) { + target.attachToClassicLB(this); + + this.newTarget(target); + } + + public get loadBalancerName() { + return this.elb.ref; + } + + public get loadBalancerCanonicalHostedZoneName() { + return this.elb.loadBalancerCanonicalHostedZoneName; + } + + public get loadBalancerDnsName() { + return this.elb.loadBalancerDnsName; + } + + public get loadBalancerSourceSecurityGroupGroupName() { + return this.elb.loadBalancerSourceSecurityGroupGroupName; + } + + public get loadBalancerSourceSecurityGroupOwnerAlias() { + return this.elb.loadBalancerSourceSecurityGroupOwnerAlias; + } + + /** + * Allow connections to all existing targets on new instance port + */ + private newInstancePort(instancePort: number) { + this.targets.forEach(t => this.allowTargetConnection(instancePort, t)); + + // Keep track of port for future targets + this.instancePorts.push(instancePort); + } + + /** + * Allow connections to target on all existing instance ports + */ + private newTarget(target: IClassicLoadBalancerTarget) { + this.instancePorts.forEach(p => this.allowTargetConnection(p, target)); + + // Keep track of target for future listeners. + this.targets.push(target); + } + + /** + * Allow connections for a single (port, target) pair + */ + private allowTargetConnection(instancePort: number, target: IClassicLoadBalancerTarget) { + this.connections.allowTo( + target, + new TcpPort(instancePort), + `Port ${instancePort} LB to fleet`); + } +} + +/** + * Reference to a listener's port just created + * + * This class exists to make it convenient to add port ranges to the load + * balancer's security group just for the port ranges that are involved in the + * listener. + */ +export class ClassicListenerPort implements IDefaultConnectable { + public readonly connections: DefaultConnections; + + constructor(securityGroup: ISecurityGroup, public readonly defaultPortRange: IPortRange) { + this.connections = new DefaultConnections(securityGroup, this); + } +} + +function wellKnownProtocol(port: number): LoadBalancingProtocol { + const proto = tryWellKnownProtocol(port); + if (!proto) { + throw new Error(`Please supply protocol to go with port ${port}`); + } + return proto; +} + +function tryWellKnownProtocol(port: number): LoadBalancingProtocol | undefined { + if (port === 80) { return LoadBalancingProtocol.Http; } + if (port === 443) { return LoadBalancingProtocol.Https; } + return undefined; +} + +function isHttpProtocol(proto: LoadBalancingProtocol): boolean { + return proto === LoadBalancingProtocol.Https || proto === LoadBalancingProtocol.Http; +} + +function ifUndefined(x: T | undefined, def: T): T { + return x != null ? x : def; +} + +function ifUndefinedLazy(x: T | undefined, def: () => T): T { + return x != null ? x : def(); +} + +/** + * Turn health check parameters into a parameter blob for the Classic LB + */ +function healthCheckToJSON(healthCheck: HealthCheck): elasticloadbalancing.LoadBalancerResource.HealthCheckProperty { + const protocol = ifUndefined(healthCheck.protocol, + ifUndefined(tryWellKnownProtocol(healthCheck.port), + LoadBalancingProtocol.Tcp)); + + const path = protocol === LoadBalancingProtocol.Http || protocol === LoadBalancingProtocol.Https ? ifUndefined(healthCheck.path, "/") : ""; + + const target = `${protocol.toUpperCase()}:${healthCheck.port}${path}`; + + return { + healthyThreshold: ifUndefined(healthCheck.healthyThreshold, 2).toString(), + interval: ifUndefined(healthCheck.interval, 30).toString(), + target, + timeout: ifUndefined(healthCheck.timeout, 5).toString(), + unhealthyThreshold: ifUndefined(healthCheck.unhealthyThreshold, 5).toString(), + }; +} diff --git a/packages/aws-cdk-ec2/lib/machine-image.ts b/packages/aws-cdk-ec2/lib/machine-image.ts new file mode 100644 index 0000000000000..363e23fb93ef2 --- /dev/null +++ b/packages/aws-cdk-ec2/lib/machine-image.ts @@ -0,0 +1,255 @@ +import { Construct, SSMParameterProvider, Stack } from 'aws-cdk'; + +/** + * Interface for classes that can select an appropriate machine image to use + */ +export interface IMachineImageSource { + /** + * Return the image to use in the given context + */ + getImage(parent: Construct): MachineImage; +} + +/** + * Select the latest version of the indicated Windows version + * + * The AMI ID is selected using the values published to the SSM parameter store. + * + * https://aws.amazon.com/blogs/mt/query-for-the-latest-windows-ami-using-systems-manager-parameter-store/ + */ +export class WindowsImage implements IMachineImageSource { + constructor(private readonly version: WindowsVersion) { + } + + /** + * Return the image to use in the given context + */ + public getImage(parent: Construct): MachineImage { + const ssmProvider = new SSMParameterProvider(parent); + + const parameterName = this.imageParameterName(this.version); + const ami = ssmProvider.getString(parameterName); + return new MachineImage(ami, new WindowsOS()); + } + + /** + * Construct the SSM parameter name for the given Windows image + */ + private imageParameterName(version: WindowsVersion): string { + return '/aws/service/ami-windows-latest/' + version; + } +} + +/** + * Amazon Linux image properties + */ +export interface AmazonLinuxImageProps { + /** + * What edition of Amazon Linux to use + * + * @default Standard + */ + edition?: AmazonLinuxEdition; + + /** + * Virtualization type + * + * @default HVM + */ + virtualization?: AmazonLinuxVirt; + + /** + * What storage backed image to use + * + * @default GeneralPurpose + */ + storage?: AmazonLinuxStorage; +} + +/** + * Selects the latest version of Amazon Linux + * + * The AMI ID is selected using the values published to the SSM parameter store. + */ +export class AmazonLinuxImage implements IMachineImageSource { + private readonly edition?: AmazonLinuxEdition; + + private readonly virtualization?: AmazonLinuxVirt; + + private readonly storage?: AmazonLinuxStorage; + + constructor(props?: AmazonLinuxImageProps) { + this.edition = (props && props.edition) || AmazonLinuxEdition.Standard; + this.virtualization = (props && props.virtualization) || AmazonLinuxVirt.HVM; + this.storage = (props && props.storage) || AmazonLinuxStorage.GeneralPurpose; + } + + /** + * Return the image to use in the given context + */ + public getImage(parent: Construct): MachineImage { + const parts: Array = [ + 'amzn-ami', + this.edition !== AmazonLinuxEdition.Standard ? this.edition : undefined, + this.virtualization, + 'x86_64', // No 32-bits images vended through this + this.storage + ].filter(x => x !== undefined); // Get rid of undefineds + + const parameterName = '/aws/service/ami-amazon-linux-latest/' + parts.join('-'); + + const ssmProvider = new SSMParameterProvider(parent); + const ami = ssmProvider.getString(parameterName); + return new MachineImage(ami, new LinuxOS()); + } +} + +/** + * Amazon Linux edition + */ +export enum AmazonLinuxEdition { + /** + * Standard edition + */ + Standard = 'standard', + + /** + * Minimal edition + */ + Minimal = 'minimal' +} + +/** + * Virtualization type for Amazon Linux + */ +export enum AmazonLinuxVirt { + /** + * HVM virtualization (recommended) + */ + HVM = 'hvm', + + /** + * PV virtualization + */ + PV = 'pv' +} + +export enum AmazonLinuxStorage { + /** + * EBS-backed storage + */ + EBS = 'ebs', + + /** + * S3-backed storage + */ + S3 = 'ebs', + + /** + * General Purpose-based storage (recommended) + */ + GeneralPurpose = 'gp2', +} + +/** + * Construct a Linux machine image from an AMI map + * + * Linux images IDs are not published to SSM parameter store yet, so you'll have to + * manually specify an AMI map. + */ +export class GenericLinuxImage implements IMachineImageSource { + constructor(private readonly amiMap: {[region: string]: string}) { + } + + public getImage(parent: Construct): MachineImage { + const stack = Stack.find(parent); + const region = stack.requireRegion('AMI cannot be determined'); + const ami = this.amiMap[region]; + if (!ami) { + throw new Error(`Unable to find AMI in AMI map: no AMI specified for region '${region}'`); + } + + return new MachineImage(ami, new LinuxOS()); + } +} + +/** + * The Windows version to use for the WindowsImage + */ +export enum WindowsVersion { + WindowsServer2016TurksihFullBase = 'Windows_Server-2016-Turkish-Full-Base', + WindowsServer2016SwedishFullBase = 'Windows_Server-2016-Swedish-Full-Base', + WindowsServer2016SpanishFullBase = 'Windows_Server-2016-Spanish-Full-Base', + WindowsServer2016RussianFullBase = 'Windows_Server-2016-Russian-Full-Base', + WindowsServer2016PortuguesePortugalFullBase = 'Windows_Server-2016-Portuguese_Portugal-Full-Base', + WindowsServer2016PortugueseBrazilFullBase = 'Windows_Server-2016-Portuguese_Brazil-Full-Base', + WindowsServer2016PolishFullBase = 'Windows_Server-2016-Polish-Full-Base', + WindowsServer2016KoreanFullSQL2016Base = 'Windows_Server-2016-Korean-Full-SQL_2016_SP1_Standard', + WindowsServer2016KoreanFullBase = 'Windows_Server-2016-Korean-Full-Base', + WindowsServer2016JapaneseFullSQL2016Web = 'Windows_Server-2016-Japanese-Full-SQL_2016_SP1_Web', + WindowsServer2016JapaneseFullSQL2016Standard = 'Windows_Server-2016-Japanese-Full-SQL_2016_SP1_Standard', + WindowsServer2016JapaneseFullSQL2016Express = 'Windows_Server-2016-Japanese-Full-SQL_2016_SP1_Express', + WindowsServer2016JapaneseFullSQL2016Enterprise = 'Windows_Server-2016-Japanese-Full-SQL_2016_SP1_Enterprise', + WindowsServer2016JapaneseFullBase = 'Windows_Server-2016-Japanese-Full-Base', + WindowsServer2016ItalianFullBase = 'Windows_Server-2016-Italian-Full-Base', + WindowsServer2016HungarianFullBase = 'Windows_Server-2016-Hungarian-Full-Base', + WindowsServer2016GermanFullBase = 'Windows_Server-2016-German-Full-Base', + WindowsServer2016FrenchFullBase = 'Windows_Server-2016-French-Full-Base', + WindowsServer2016EnglishNanoBase = 'Windows_Server-2016-English-Nano-Base', + WindowsServer2016EnglishFullSQL2017Web = 'Windows_Server-2016-English-Full-SQL_2017_Web', + WindowsServer2016EnglishFullSQL2017Standard = 'Windows_Server-2016-English-Full-SQL_2017_Standard', + WindowsServer2016EnglishFullSQL2017Express = 'Windows_Server-2016-English-Full-SQL_2017_Express', + WindowsServer2016EnglishFullSQL2017Enterprise = 'Windows_Server-2016-English-Full-SQL_2017_Enterprise', +} + +/** + * Representation of a machine to be launched + * + * Combines an AMI ID with an OS. + */ +export class MachineImage { + constructor(public readonly imageId: string, public readonly os: OperatingSystem) { + } +} + +/** + * The OS type of a particular image + */ +export enum OperatingSystemType { + Linux, + Windows, +} + +/** + * Abstraction of OS features we need to be aware of + */ +export abstract class OperatingSystem { + public abstract createUserData(scripts: string[]): string; + abstract get type(): OperatingSystemType; +} + +/** + * OS features specialized for Windows + */ +export class WindowsOS extends OperatingSystem { + public createUserData(scripts: string[]): string { + return `${scripts.join('\n')}`; + } + + get type(): OperatingSystemType { + return OperatingSystemType.Windows; + } +} + +/** + * OS features specialized for Linux + */ +export class LinuxOS extends OperatingSystem { + public createUserData(scripts: string[]): string { + return '#!/bin/bash\n' + scripts.join('\n'); + } + + get type(): OperatingSystemType { + return OperatingSystemType.Linux; + } +} \ No newline at end of file diff --git a/packages/aws-cdk-ec2/lib/network-util.ts b/packages/aws-cdk-ec2/lib/network-util.ts new file mode 100644 index 0000000000000..0ad284d5d59a2 --- /dev/null +++ b/packages/aws-cdk-ec2/lib/network-util.ts @@ -0,0 +1,89 @@ +/** + * InvalidCidrRangeError is thrown when attempting to perform operations on a CIDR + * range that is either not valid, or outside of the VPC size limits. + */ +export class InvalidCidrRangeError extends Error { + constructor(cidr: string) { + super(cidr + ' is not a valid VPC CIDR range (must be between /16 and /28)'); + // The following line is required for type checking of custom errors, and must be called right after super() + // https://stackoverflow.com/questions/31626231/custom-error-class-in-typescript + Object.setPrototypeOf(this, InvalidCidrRangeError.prototype); + } +} + +/** + * InvalidSubnetCountError is thrown when attempting to split a CIDR range into more + * subnets than it has IP space for. + */ +export class InvalidSubnetCountError extends Error { + constructor(cidr: string, count: number) { + super('VPC range (' + cidr + ') does not have enough IP space to be split into ' + count + ' subnets'); + // The following line is required for type checking of custom errors, and must be called right after super() + // https://stackoverflow.com/questions/31626231/custom-error-class-in-typescript + Object.setPrototypeOf(this, InvalidSubnetCountError.prototype); + } +} + +/** + * NetworkUtils contains helpers to work with network constructs (subnets/ranges) + */ +export class NetworkUtils { + + /** + * + * splitCIDR takes a CIDR range (e.g. 10.0.0.0/16) and splits it into + * the provided number of smaller subnets (eg 2 of 10.0.0.0/17). + * + * @param {string} cidr The CIDR range to split (e.g. 10.0.0.0/16) + * @param {number} subnetCount How many subnets to create (min:2 max:30) + * @returns Array An array of CIDR strings (e.g. [ '10.0.0.0/17', '10.0.128.0/17' ]) + */ + public static splitCIDR(cidr: string, subnetCount: number): string[] { + + const parts = cidr.toString().split(/([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)\/([0-9]+)/); + + if (parts.length !== 7) { + throw new InvalidCidrRangeError(cidr); + } + + const range = parseInt(parts[5], 10); + const rangeHosts = Math.pow(2, (32 - range)); + const subnetSize = range + Math.round((subnetCount / 2)); + const subnetHosts = Math.pow(2, (32 - subnetSize)); + + // Ensure the VPC cidr range fits within the EC2 VPC parameter ranges + if (range < 16 || range > 28) { + throw new InvalidCidrRangeError(cidr); + } + + // Ensure the resulting subnet size is within the EC2 VPC parameter ranges + if (subnetSize < 16 || subnetSize > 28) { + throw new InvalidSubnetCountError(cidr, subnetCount); + } + + // Check that the requested number of subnets fits into the provided CIDR range + if (subnetHosts === 0 || subnetHosts * subnetCount > rangeHosts) { + throw new InvalidSubnetCountError(cidr, subnetCount); + } + + // Convert the initial CIDR to decimal format + const rangeDec = ((((((+parts[1]) * 256) + (+parts[2])) * 256) + (+parts[3])) * 256) + (+parts[4]); + + // Generate each of the subnets required + const subnets: string[] = []; + for (let i = 0; i < subnetCount; i++) { + const subnetDec = rangeDec + (i * subnetHosts); + // tslint:disable:no-bitwise + const p1 = subnetDec & 255; + const p2 = ((subnetDec >> 8) & 255); + const p3 = ((subnetDec >> 16) & 255); + const p4 = ((subnetDec >> 24) & 255); + // tslint:enable:no-bitwise + subnets.push(p4 + '.' + p3 + '.' + p2 + '.' + p1 + '/' + subnetSize); + } + + return subnets; + + } + +} diff --git a/packages/aws-cdk-ec2/lib/security-group.ts b/packages/aws-cdk-ec2/lib/security-group.ts new file mode 100644 index 0000000000000..ba0ff15208f75 --- /dev/null +++ b/packages/aws-cdk-ec2/lib/security-group.ts @@ -0,0 +1,257 @@ +import { Construct, Token } from 'aws-cdk'; +import { ec2 } from 'aws-cdk-resources'; +import { IConnectionPeer, IPortRange } from './connection'; +import { slugify } from './util'; +import { VpcNetworkRef } from './vpc-ref'; + +export interface SecurityGroupRefProps { + /** + * ID of security group + */ + securityGroupId: ec2.SecurityGroupId; +} + +/** + * Basic interface for security groups + */ +export interface ISecurityGroup extends IConnectionPeer { + readonly securityGroupId: ec2.SecurityGroupId; + readonly canInlineRule: boolean; + + addIngressRule(peer: IConnectionPeer, connection: IPortRange, description: string): void; + addEgressRule(peer: IConnectionPeer, connection: IPortRange, description: string): void; +} + +/** + * A SecurityGroup that is not created in this template + */ +export class SecurityGroupRef extends Construct implements ISecurityGroup { + public readonly securityGroupId: ec2.SecurityGroupId; + public readonly canInlineRule = false; + + constructor(parent: Construct, name: string, props: SecurityGroupRefProps) { + super(parent, name); + + this.securityGroupId = props.securityGroupId; + } + + public addIngressRule(peer: IConnectionPeer, connection: IPortRange, description: string) { + new ec2.SecurityGroupIngressResource(this, slugify(description), { + groupId: this.securityGroupId, + ...peer.toIngressRuleJSON(), + ...connection.toRuleJSON(), + description + }); + } + + public addEgressRule(peer: IConnectionPeer, connection: IPortRange, description: string) { + new ec2.SecurityGroupEgressResource(this, slugify(description), { + groupId: this.securityGroupId, + ...peer.toEgressRuleJSON(), + ...connection.toRuleJSON(), + description + }); + } + + public toIngressRuleJSON(): any { + return { sourceSecurityGroupId: this.securityGroupId }; + } + + public toEgressRuleJSON(): any { + return { destinationSecurityGroupId: this.securityGroupId }; + } +} + +export interface SecurityGroupProps { + /** + * The name of the security group. For valid values, see the GroupName + * parameter of the CreateSecurityGroup action in the Amazon EC2 API + * Reference. + * + * It is not recommended to use an explicit group name. + * + * @default If you don't specify a GroupName, AWS CloudFormation generates a + * unique physical ID and uses that ID for the group name. + */ + groupName?: string; + + /** + * A description of the security group. + * + * @default The default name will be the construct's CDK path. + */ + description?: string; + + /** + * The VPC in which to create the security group. + */ + vpc: VpcNetworkRef; +} + +/** + * Creates an Amazon EC2 security group within a VPC. + * + * This class has an additional optimization over SecurityGroupRef that it can also create + * inline ingress and egress rule (which saves on the total number of resources inside + * the template). + */ +export class SecurityGroup extends SecurityGroupRef { + /** + * An attribute that represents the security group name. + */ + public readonly groupName: SecurityGroupName; + + /** + * An attribute that represents the physical VPC ID this security group is part of. + */ + public readonly vpcId: ec2.SecurityGroupVpcId; + + private readonly securityGroup: ec2.SecurityGroupResource; + private readonly directIngressRules: ec2.SecurityGroupResource.IngressProperty[] = []; + private readonly directEgressRules: ec2.SecurityGroupResource.EgressProperty[] = []; + + constructor(parent: Construct, name: string, props: SecurityGroupProps) { + super(parent, name, { securityGroupId: new Token(() => this.securityGroup.securityGroupId) }); + + const groupDescription = props.description || this.path; + + this.securityGroup = new ec2.SecurityGroupResource(this, 'Resource', { + groupName: props.groupName, + groupDescription, + securityGroupIngress: new Token(() => this.directIngressRules), + securityGroupEgress: new Token(() => this.directEgressRules), + vpcId: props.vpc.vpcId, + }); + + this.groupName = this.securityGroup.ref; + this.vpcId = this.securityGroup.securityGroupVpcId; + } + + public addIngressRule(peer: IConnectionPeer, connection: IPortRange, description: string) { + if (!peer.canInlineRule || !connection.canInlineRule) { + super.addIngressRule(peer, connection, description); + return; + } + + this.addDirectIngressRule({ + ...peer.toIngressRuleJSON(), + ...connection.toRuleJSON(), + description + }); + } + + public addEgressRule(peer: IConnectionPeer, connection: IPortRange, description: string) { + if (!peer.canInlineRule || !connection.canInlineRule) { + super.addEgressRule(peer, connection, description); + return; + } + + this.addDirectEgressRule({ + ...peer.toIngressRuleJSON(), + ...connection.toRuleJSON(), + description + }); + } + + /** + * Add a direct ingress rule + */ + private addDirectIngressRule(rule: ec2.SecurityGroupResource.IngressProperty) { + if (!this.hasIngressRule(rule)) { + this.directIngressRules.push(rule); + } + } + + /** + * Return whether the given ingress rule exists on the group + */ + private hasIngressRule(rule: ec2.SecurityGroupResource.IngressProperty): boolean { + return this.directIngressRules.findIndex(r => ingressRulesEqual(r, rule)) > -1; + } + + /** + * Add a direct egress rule + */ + private addDirectEgressRule(rule: ec2.SecurityGroupResource.EgressProperty) { + if (!this.hasEgressRule(rule)) { + this.directEgressRules.push(rule); + } + } + + /** + * Return whether the given egress rule exists on the group + */ + private hasEgressRule(rule: ec2.SecurityGroupResource.EgressProperty): boolean { + return this.directEgressRules.findIndex(r => egressRulesEqual(r, rule)) > -1; + } +} + +export class SecurityGroupName extends Token { } + +export interface ConnectionRule { + /** + * The IP protocol name (tcp, udp, icmp) or number (see Protocol Numbers). + * Use -1 to specify all protocols. If you specify -1, or a protocol number + * other than tcp, udp, icmp, or 58 (ICMPv6), traffic on all ports is + * allowed, regardless of any ports you specify. For tcp, udp, and icmp, you + * must specify a port range. For protocol 58 (ICMPv6), you can optionally + * specify a port range; if you don't, traffic for all types and codes is + * allowed. + * + * @default tcp + */ + protocol?: string; + + /** + * Start of port range for the TCP and UDP protocols, or an ICMP type number. + * + * If you specify icmp for the IpProtocol property, you can specify + * -1 as a wildcard (i.e., any ICMP type number). + */ + fromPort: number; + + /** + * End of port range for the TCP and UDP protocols, or an ICMP code. + * + * If you specify icmp for the IpProtocol property, you can specify -1 as a + * wildcard (i.e., any ICMP code). + * + * @default If toPort is not specified, it will be the same as fromPort. + */ + toPort?: number; + + /** + * Description of this connection. It is applied to both the ingress rule + * and the egress rule. + * + * @default No description + */ + description?: string; +} + +/** + * Compare two ingress rules for equality the same way CloudFormation would (discarding description) + */ +function ingressRulesEqual(a: ec2.SecurityGroupResource.IngressProperty, b: ec2.SecurityGroupResource.IngressProperty) { + return a.cidrIp === b.cidrIp + && a.cidrIpv6 === b.cidrIpv6 + && a.fromPort === b.fromPort + && a.toPort === b.toPort + && a.ipProtocol === b.ipProtocol + && a.sourceSecurityGroupId === b.sourceSecurityGroupId + && a.sourceSecurityGroupName === b.sourceSecurityGroupName + && a.sourceSecurityGroupOwnerId === b.sourceSecurityGroupOwnerId; +} + +/** + * Compare two egress rules for equality the same way CloudFormation would (discarding description) + */ +function egressRulesEqual(a: ec2.SecurityGroupResource.EgressProperty, b: ec2.SecurityGroupResource.EgressProperty) { + return a.cidrIp === b.cidrIp + && a.cidrIpv6 === b.cidrIpv6 + && a.fromPort === b.fromPort + && a.toPort === b.toPort + && a.ipProtocol === b.ipProtocol + && a.destinationPrefixListId === b.destinationPrefixListId + && a.destinationSecurityGroupId === b.destinationSecurityGroupId; +} diff --git a/packages/aws-cdk-ec2/lib/util.ts b/packages/aws-cdk-ec2/lib/util.ts new file mode 100644 index 0000000000000..66b377baf1681 --- /dev/null +++ b/packages/aws-cdk-ec2/lib/util.ts @@ -0,0 +1,41 @@ +import { Token } from 'aws-cdk'; + +export function normalizeStackParameters(props: any) { + const params: { [key: string]: any } = { }; + + for (const prop of Object.keys(props)) { + let value = (props as any)[prop]; + + if (Array.isArray(value)) { + // We can't ACTUALLY pass token values here (must be literal strings), but in the case where this is a MissingContext + // token we also don't want to fail, since the template will be resynthesized later + // FIXME: Make a distinction between those two cases. + value = value.map(el => el instanceof Token ? "(token value)" : el).join(','); + } else if (typeof value === 'boolean') { + value = value.toString(); + } else if (typeof value === 'object' && !(value instanceof Token)) { + throw new Error(`Object parameters are not supported for property ${prop}: ${JSON.stringify(value)}`); + } else if (typeof value === 'function') { + throw new Error(`Property ${prop} is a function`); + } + + // Since we're going to plug this into a stack template that is expecting + // pascalcased parameter names, uppercase the first letter here. + params[upperCaseFirst(prop)] = value; + } + + return params; +} + +function upperCaseFirst(x: string): string { + return x.substr(0, 1).toUpperCase() + x.substr(1); +} + +/** + * Turn an arbitrary string into one that can be used as a CloudFormation identifier by stripping special characters + * + * (At the moment, no efforts are taken to prevent collissions, but we can add that later when it becomes necessary). + */ +export function slugify(x: string): string { + return x.replace(/[^a-zA-Z0-9]/g, ''); +} diff --git a/packages/aws-cdk-ec2/lib/vpc-ref.ts b/packages/aws-cdk-ec2/lib/vpc-ref.ts new file mode 100644 index 0000000000000..14020c7e54418 --- /dev/null +++ b/packages/aws-cdk-ec2/lib/vpc-ref.ts @@ -0,0 +1,227 @@ +import { Construct, IDependable, Output, StringListOutput, Token } from "aws-cdk"; + +/** + * Customize how instances are placed inside a VPC + * + * Constructs that allow customization of VPC placement use parameters of this + * type to provide placement settings. + */ +export interface VpcPlacementStrategy { + /** + * Whether to use the VPC's public subnets to start instances + * + * If false, the instances are started in the private subnets. + * + * @default false + */ + usePublicSubnets?: boolean; +} + +/** + * A new or imported VPC + */ +export abstract class VpcNetworkRef extends Construct implements IDependable { + /** + * Import an exported VPC + */ + public static import(parent: Construct, name: string, props: VpcNetworkRefProps) { + return new ImportedVpcNetwork(parent, name, props); + } + + /** + * Identifier for this VPC + */ + public abstract readonly vpcId: VpcNetworkId; + + /** + * List of public subnets in this VPC + */ + public abstract readonly publicSubnets: VpcSubnetRef[]; + + /** + * List of private subnets in this VPC + */ + public abstract readonly privateSubnets: VpcSubnetRef[]; + + /** + * Parts of the VPC that constitute full construction + */ + public readonly dependencyElements: IDependable[] = []; + + /** + * Return the subnets appropriate for the placement strategy + */ + public subnets(placement?: VpcPlacementStrategy): VpcSubnetRef[] { + if (!placement) { return this.privateSubnets; } + return placement.usePublicSubnets ? this.publicSubnets : this.privateSubnets; + } + + /** + * Export this VPC from the stack + */ + public export(): VpcNetworkRefProps { + return { + vpcId: new Output(this, 'VpcId', { value: this.vpcId }).makeImportValue(), + availabilityZones: this.publicSubnets.map(s => s.availabilityZone), + publicSubnetIds: new StringListOutput(this, 'PublicSubnetIDs', { values: this.publicSubnets.map(s => s.subnetId) }).makeImportValues(), + privateSubnetIds: new StringListOutput(this, 'PrivateSubnetIDs', { values: this.privateSubnets.map(s => s.subnetId) }).makeImportValues(), + }; + } +} + +/** + * An imported VpcNetwork + */ +class ImportedVpcNetwork extends VpcNetworkRef { + /** + * Identifier for this VPC + */ + public readonly vpcId: VpcNetworkId; + + /** + * List of public subnets in this VPC + */ + public readonly publicSubnets: VpcSubnetRef[]; + + /** + * List of private subnets in this VPC + */ + public readonly privateSubnets: VpcSubnetRef[]; + + constructor(parent: Construct, name: string, props: VpcNetworkRefProps) { + super(parent, name); + + this.vpcId = props.vpcId; + + if (props.availabilityZones.length !== props.publicSubnetIds.length) { + throw new Error('Availability zone and public subnet ID arrays must be same length'); + } + + if (props.availabilityZones.length !== props.privateSubnetIds.length) { + throw new Error('Availability zone and private subnet ID arrays must be same length'); + } + + const n = props.availabilityZones.length; + this.publicSubnets = range(n).map(i => VpcSubnetRef.import(this, `PublicSubnet${i}`, { + availabilityZone: props.availabilityZones[i], + subnetId: props.publicSubnetIds[i] + })); + this.privateSubnets = range(n).map(i => VpcSubnetRef.import(this, `PrivateSubnet${i}`, { + availabilityZone: props.availabilityZones[i], + subnetId: props.privateSubnetIds[i] + })); + } +} + +/** + * Properties that reference an external VpcNetwork + */ +export interface VpcNetworkRefProps { + /** + * VPC's identifier + */ + vpcId: VpcNetworkId; + + /** + * List of a availability zones, one for every subnet. + * + * The first half are for the public subnets, the second half are for + * the private subnets. + */ + availabilityZones: string[]; + + /** + * List of public subnet IDs, one for every subnet + * + * Must match the availability zones and private subnet ids in length and order. + */ + publicSubnetIds: VpcSubnetId[]; + + /** + * List of private subnet IDs, one for every subnet + * + * Must match the availability zones and public subnet ids in length and order. + */ + privateSubnetIds: VpcSubnetId[]; +} + +/** + * Identifier for a VPC + */ +export class VpcNetworkId extends Token { +} + +/** + * A new or imported VPC Subnet + */ +export abstract class VpcSubnetRef extends Construct implements IDependable { + public static import(parent: Construct, name: string, props: VpcSubnetRefProps) { + return new ImportedVpcSubnet(parent, name, props); + } + + /** + * The Availability Zone the subnet is located in + */ + public abstract readonly availabilityZone: string; + + /** + * The subnetId for this particular subnet + */ + public abstract readonly subnetId: VpcSubnetId; + + /** + * Parts of this VPC subnet + */ + public readonly dependencyElements: IDependable[] = []; +} + +/** + * Subnet of an imported VPC + */ +class ImportedVpcSubnet extends VpcSubnetRef { + /** + * The Availability Zone the subnet is located in + */ + public readonly availabilityZone: string; + + /** + * The subnetId for this particular subnet + */ + public readonly subnetId: VpcSubnetId; + + constructor(parent: Construct, name: string, props: VpcSubnetRefProps) { + super(parent, name); + + this.availabilityZone = props.availabilityZone; + this.subnetId = props.subnetId; + } +} + +export interface VpcSubnetRefProps { + /** + * The Availability Zone the subnet is located in + */ + availabilityZone: string; + + /** + * The subnetId for this particular subnet + */ + subnetId: VpcSubnetId; +} + +/** + * Id of a VPC Subnet + */ +export class VpcSubnetId extends Token { +} + +/** + * Generate the list of numbers of [0..n) + */ +function range(n: number): number[] { + const ret: number[] = []; + for (let i = 0; i < n; i++) { + ret.push(i); + } + return ret; +} \ No newline at end of file diff --git a/packages/aws-cdk-ec2/lib/vpc.ts b/packages/aws-cdk-ec2/lib/vpc.ts new file mode 100644 index 0000000000000..474bad07a1228 --- /dev/null +++ b/packages/aws-cdk-ec2/lib/vpc.ts @@ -0,0 +1,385 @@ +import { AvailabilityZoneProvider, Construct, Tag, Token } from 'aws-cdk'; +import { ec2 } from 'aws-cdk-resources'; +import { NetworkUtils } from './network-util'; +import { VpcNetworkId, VpcNetworkRef, VpcSubnetId, VpcSubnetRef } from './vpc-ref'; +/** + * VpcNetworkProps allows you to specify configuration options for a VPC + */ +export interface VpcNetworkProps { + + /** + * The CIDR range to use for the VPC (e.g. '10.0.0.0/16'). Should be a minimum of /28 and maximum size of /16. + * The range will be split evenly into two subnets per Availability Zone (one public, one private). + */ + cidr?: string; + + /** + * Indicates whether the instances launched in the VPC get public DNS hostnames. + * If this attribute is true, instances in the VPC get public DNS hostnames, + * but only if the enableDnsSupport attribute is also set to true. + */ + enableDnsHostnames?: boolean; + + /** + * Indicates whether the DNS resolution is supported for the VPC. If this attribute + * is false, the Amazon-provided DNS server in the VPC that resolves public DNS hostnames + * to IP addresses is not enabled. If this attribute is true, queries to the Amazon + * provided DNS server at the 169.254.169.253 IP address, or the reserved IP address + * at the base of the VPC IPv4 network range plus two will succeed. + */ + enableDnsSupport?: boolean; + + /** + * The default tenancy of instances launched into the VPC. + * By default, instances will be launched with default (shared) tenancy. + * By setting this to dedicated tenancy, instances will be launched on hardware dedicated + * to a single AWS customer, unless specifically specified at instance launch time. + * Please note, not all instance types are usable with Dedicated tenancy. + */ + defaultInstanceTenancy?: DefaultInstanceTenancy; + + /** + * The AWS resource tags to associate with the VPC. + */ + tags?: Tag[]; + + /** + * Defines whether the VPC is configured to route outbound traffic from private and/or public subnets. + * By default, outbound traffic will be allowed from public and private subnets. + */ + outboundTraffic?: OutboundTrafficMode; + + /** + * Define the maximum number of AZs to use in this region + * + * If the region has more AZs than you want to use (for example, because of EIP limits), + * pick a lower number here. The AZs will be sorted and picked from the start of the list. + * + * @default All AZs in the region + */ + maxAZs?: number; +} + +/** + * The default tenancy of instances launched into the VPC. + */ +export enum DefaultInstanceTenancy { + /** + * Instances can be launched with any tenancy. + */ + Default = 'default', + + /** + * Any instance launched into the VPC automatically has dedicated tenancy, unless you launch it with the default tenancy. + */ + Dedicated = 'dedicated' +} + +/** + * The outbound traffic mode defines whether the VPC is configured to route outbound traffic. + */ +export enum OutboundTrafficMode { + + /** + * Outbound traffic is not routed. No Internet Gateway (IGW) will be deployed, and no NAT Gateways will be deployed. + */ + None = 1, + + /** + * Outbound traffic will be routed from public subnets via an Internet Gateway. + * Outbound traffic from private subnets will not be routed. + */ + FromPublicSubnetsOnly = 2, + + /** + * Outbound traffic from public subnets will be routed via an Internet Gateway. + * Outbound traffic from private subnets will be routed via a set of NAT Gateways (1 per AZ). + */ + FromPublicAndPrivateSubnets = 3 +} + +/** + * VpcNetwork deploys an AWS VPC, with public and private subnets per Availability Zone. + * For example: + * + * import { VpcNetwork } from 'aws-cdk-ec2' + * + * const vpc = new VpcNetwork(this, { + * cidr: "10.0.0.0/16" + * }) + * + * // Iterate the public subnets + * for (let subnet of vpc.publicSubnets) { + * + * } + * + * // Iterate the private subnets + * for (let subnet of vpc.privateSubnets) { + * + * } + */ +export class VpcNetwork extends VpcNetworkRef { + + /** + * The default CIDR range used when creating VPCs. + * This can be overridden using VpcNetworkProps when creating a VPCNetwork resource. + * e.g. new VpcResource(this, { cidr: '192.168.0.0./16' }) + */ + public static readonly DEFAULT_CIDR_RANGE = '10.0.0.0/16'; + + /** + * Identifier for this VPC + */ + public readonly vpcId: VpcNetworkId; + + /** + * List of public subnets in this VPC + */ + public readonly publicSubnets: VpcSubnetRef[] = []; + + /** + * List of private subnets in this VPC + */ + public readonly privateSubnets: VpcSubnetRef[] = []; + + /** + * The VPC resource + */ + private resource: ec2.VPCResource; + + /** + * VpcNetwork creates a VPC that spans a whole region. + * It will automatically divide the provided VPC CIDR range, and create public and private subnets per Availability Zone. + * Network routing for the public subnets will be configured to allow outbound access directly via an Internet Gateway. + * Network routing for the private subnets will be configured to allow outbound access via a set of resilient NAT Gateways (one per AZ). + */ + constructor(parent: Construct, name: string, props: VpcNetworkProps = {}) { + super(parent, name); + + // Can't have enabledDnsHostnames without enableDnsSupport + if (props.enableDnsHostnames && !props.enableDnsSupport) { + throw new Error('To use DNS Hostnames, DNS Support must be enabled, however, it was explicitly disabled.'); + } + + const cidrBlock = props.cidr || VpcNetwork.DEFAULT_CIDR_RANGE; + const enableDnsHostnames = props.enableDnsHostnames == null ? true : props.enableDnsHostnames; + const enableDnsSupport = props.enableDnsSupport == null ? true : props.enableDnsSupport; + const instanceTenancy = props.defaultInstanceTenancy || 'default'; + const tags = props.tags || []; + const outboundTraffic = props.outboundTraffic || OutboundTrafficMode.FromPublicAndPrivateSubnets; + + // Define a VPC using the provided CIDR range + this.resource = new ec2.VPCResource(this, 'Resource', { + cidrBlock, + enableDnsHostnames, + enableDnsSupport, + instanceTenancy, + tags + }); + + this.vpcId = this.resource.ref; + this.dependencyElements.push(this.resource); + + const allowOutbound = + outboundTraffic === OutboundTrafficMode.FromPublicSubnetsOnly || + outboundTraffic === OutboundTrafficMode.FromPublicAndPrivateSubnets; + + // Create public and private subnets in each AZ + this.createSubnets(cidrBlock, outboundTraffic, props.maxAZs); + + // Create an Internet Gateway and attach it (if the outbound traffic mode != None) + if (allowOutbound) { + const igw = new ec2.InternetGatewayResource(this, 'IGW'); + const att = new ec2.VPCGatewayAttachmentResource(this, 'VPCGW', { + internetGatewayId: igw.ref, + vpcId: this.resource.ref + }); + (this.publicSubnets as VpcPublicSubnet[]).forEach(publicSubnet => { + publicSubnet.addDefaultIGWRouteEntry(igw.ref); + }); + + this.dependencyElements.push(igw, att); + } + } + + /** + * @returns {Token} The IPv4 CidrBlock as returned by the VPC + */ + public get cidr(): Token { + return this.resource.getAtt("CidrBlock"); + } + + /** + * createSubnets takes a VPC, and creates a public and private subnet + * in each Availability Zone. + */ + private createSubnets(cidr: string, outboundTraffic: OutboundTrafficMode, maxAZs?: number) { + + // Calculate number of public/private subnets based on number of AZs + const zones = new AvailabilityZoneProvider(this).availabilityZones; + zones.sort(); + + // Restrict to maxAZs if given + if (maxAZs != null) { + zones.splice(maxAZs); + } + + // Split the CIDR range into each availablity zone + const ranges = NetworkUtils.splitCIDR(cidr, zones.length); + + for (let i = 0; i < zones.length; i++) { + this.createSubnetPair(ranges[i], zones[i], i + 1, outboundTraffic); + } + + } + + /** + * Creates a public and private subnet, as well as the needed nat gateway and default route, if necessary. + */ + private createSubnetPair(azCidr: string, zone: string, index: number, outboundTraffic: OutboundTrafficMode) { + // Split the CIDR range for this AZ into two (public and private) + const subnetRanges = NetworkUtils.splitCIDR(azCidr, 2); + const publicSubnet = new VpcPublicSubnet(this, `PublicSubnet${index}`, { + mapPublicIpOnLaunch: true, + vpcId: this.vpcId, + availabilityZone: zone, + cidrBlock: subnetRanges[0] + }); + const privateSubnet = new VpcPrivateSubnet(this, `PrivateSubnet${index}`, { + mapPublicIpOnLaunch: false, + vpcId: this.vpcId, + availabilityZone: zone, + cidrBlock: subnetRanges[1] + }); + + // If outbound traffic from private subnets is configured, also configure NAT Gateways + // in each public subnet, and configure the default route for the private subnet via them. + if (outboundTraffic === OutboundTrafficMode.FromPublicAndPrivateSubnets) { + const ngwId = publicSubnet.addNatGateway(); + privateSubnet.addDefaultNatRouteEntry(ngwId); + } + + this.publicSubnets.push(publicSubnet); + this.privateSubnets.push(privateSubnet); + this.dependencyElements.push(publicSubnet, privateSubnet); + } + +} + +/** + * Specify configuration parameters for a VPC subnet + */ +export interface VpcSubnetProps { + availabilityZone: string; + vpcId: Token; + cidrBlock: string; + mapPublicIpOnLaunch?: boolean; +} + +/** + * Represents a new VPC subnet resource + */ +export class VpcSubnet extends VpcSubnetRef { + /** + * The Availability Zone the subnet is located in + */ + public readonly availabilityZone: string; + + /** + * The subnetId for this particular subnet + */ + public readonly subnetId: VpcSubnetId; + + /** + * The routeTableId attached to this subnet. + */ + private readonly routeTableId: Token; + + constructor(parent: Construct, name: string, props: VpcSubnetProps) { + super(parent, name); + this.availabilityZone = props.availabilityZone; + const subnet = new ec2.SubnetResource(this, 'Subnet', { + vpcId: props.vpcId, + cidrBlock: props.cidrBlock, + availabilityZone: props.availabilityZone, + mapPublicIpOnLaunch: props.mapPublicIpOnLaunch, + }); + this.subnetId = subnet.ref; + const table = new ec2.RouteTableResource(this, 'RouteTable', { + vpcId: props.vpcId, + }); + this.routeTableId = table.ref; + + // Associate the public route table for this subnet, to this subnet + const routeAssoc = new ec2.SubnetRouteTableAssociationResource(this, 'RouteTableAssociatioin', { + subnetId: this.subnetId, + routeTableId: table.ref + }); + + this.dependencyElements.push(subnet, table, routeAssoc); + } + + protected addDefaultRouteToNAT(natGatewayId: Token) { + new ec2.RouteResource(this, `DefaultRoute`, { + routeTableId: this.routeTableId, + destinationCidrBlock: '0.0.0.0/0', + natGatewayId + }); + } + + protected addDefaultRouteToIGW(gatewayId: Token) { + new ec2.RouteResource(this, `DefaultRoute`, { + routeTableId: this.routeTableId, + destinationCidrBlock: '0.0.0.0/0', + gatewayId + }); + } +} + +/** + * Represents a public VPC subnet resource + */ +export class VpcPublicSubnet extends VpcSubnet { + constructor(parent: Construct, name: string, props: VpcSubnetProps) { + super(parent, name, props); + } + + /** + * Create a default route that points to a passed IGW + */ + public addDefaultIGWRouteEntry(gatewayId: Token) { + this.addDefaultRouteToIGW(gatewayId); + } + + /** + * Creates a new managed NAT gateway attached to this public subnet. + * Also adds the EIP for the managed NAT. + * Returns the NAT Gateway ref + */ + public addNatGateway() { + // Create a NAT Gateway in this public subnet + const ngw = new ec2.NatGatewayResource(this, `NATGateway`, { + subnetId: this.subnetId, + allocationId: new ec2.EIPResource(this, `EIP`, { + domain: 'vpc' + }).eipAllocationId + }); + return ngw.ref; + } +} + +/** + * Represents a private VPC subnet resource + */ +export class VpcPrivateSubnet extends VpcSubnet { + constructor(parent: Construct, name: string, props: VpcSubnetProps) { + super(parent, name, props); + } + + /** + * Adds an entry to this subnets route table that points to the passed NATGatwayId + */ + public addDefaultNatRouteEntry(natGatewayId: Token) { + this.addDefaultRouteToNAT(natGatewayId); + } +} diff --git a/packages/aws-cdk-ec2/package-lock.json b/packages/aws-cdk-ec2/package-lock.json new file mode 100644 index 0000000000000..d8ee46ede1137 --- /dev/null +++ b/packages/aws-cdk-ec2/package-lock.json @@ -0,0 +1,3349 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@types/node": { + "version": "8.10.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.17.tgz", + "integrity": "sha512-3N3FRd/rA1v5glXjb90YdYUa+sOB7WrkU2rAhKZnF4TKD86Cym9swtulGuH0p9nxo7fP5woRNa8b0oFTpCO1bg==" + }, + "@types/nodeunit": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/nodeunit/-/nodeunit-0.0.30.tgz", + "integrity": "sha1-SNLCcZoRjHcjuDMGw+gAsRor9ng=" + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "1.0.3" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "bind-obj-methods": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bind-obj-methods/-/bind-obj-methods-1.0.0.tgz", + "integrity": "sha1-T1l5ysFXk633DkiBYeRj4gnKUJw=" + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "clean-yaml-object": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/clean-yaml-object/-/clean-yaml-object-0.1.0.tgz", + "integrity": "sha1-Y/sRDcLOGoTcIfbZM0h20BCui2g=" + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "coveralls": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-2.13.3.tgz", + "integrity": "sha512-iiAmn+l1XqRwNLXhW8Rs5qHZRFMYp9ZIPjEOVRpC/c4so6Y/f4/lFi0FfR5B9cCqgyhkJ5cZmbvcVRfP8MHchw==", + "requires": { + "js-yaml": "3.6.1", + "lcov-parse": "0.0.10", + "log-driver": "1.2.5", + "minimist": "1.2.0", + "request": "2.79.0" + }, + "dependencies": { + "js-yaml": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", + "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", + "requires": { + "argparse": "1.0.10", + "esprima": "2.7.3" + } + } + } + }, + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "requires": { + "lru-cache": "4.1.3", + "which": "1.3.1" + } + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "diff": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "ejs": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", + "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "events-to-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz", + "integrity": "sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=" + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "foreground-child": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", + "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "requires": { + "cross-spawn": "4.0.2", + "signal-exit": "3.0.2" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "fs-exists-cached": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-exists-cached/-/fs-exists-cached-1.0.0.tgz", + "integrity": "sha1-zyVVTKBQ3EmuZla0HeQiWJidy84=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "function-loop": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/function-loop/-/function-loop-1.0.1.tgz", + "integrity": "sha1-gHa7MF6OajzO7ikgdl8zDRkPNAw=" + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "requires": { + "is-property": "1.0.2" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "requires": { + "chalk": "1.1.3", + "commander": "2.15.1", + "is-my-json-valid": "2.17.2", + "pinkie-promise": "2.0.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.14.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==" + }, + "is-my-json-valid": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", + "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "is-my-ip-valid": "1.0.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + } + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "js-yaml": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", + "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", + "requires": { + "argparse": "1.0.10", + "esprima": "4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "lcov-parse": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=" + }, + "log-driver": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", + "integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY=" + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nodeunit": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/nodeunit/-/nodeunit-0.11.2.tgz", + "integrity": "sha512-rlr0Fgd66nLmWwgVFj40TZp5jo47/YqaPQtoHG78mt+DVQhaLhA8EJJYCf2lozgYplPv+jJMLt8bCP34zo05mQ==", + "requires": { + "ejs": "2.6.1", + "tap": "10.7.3" + } + }, + "nyc": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-11.8.0.tgz", + "integrity": "sha512-PUFq1PSsx5OinSk5g5aaZygcDdI3QQT5XUlbR9QRMihtMS6w0Gm8xj4BxmKeeAlpQXC5M2DIhH16Y+KejceivQ==", + "requires": { + "archy": "1.0.0", + "arrify": "1.0.1", + "caching-transform": "1.0.1", + "convert-source-map": "1.5.1", + "debug-log": "1.0.1", + "default-require-extensions": "1.0.0", + "find-cache-dir": "0.1.1", + "find-up": "2.1.0", + "foreground-child": "1.5.6", + "glob": "7.1.2", + "istanbul-lib-coverage": "1.2.0", + "istanbul-lib-hook": "1.1.0", + "istanbul-lib-instrument": "1.10.1", + "istanbul-lib-report": "1.1.3", + "istanbul-lib-source-maps": "1.2.3", + "istanbul-reports": "1.4.0", + "md5-hex": "1.3.0", + "merge-source-map": "1.1.0", + "micromatch": "3.1.10", + "mkdirp": "0.5.1", + "resolve-from": "2.0.0", + "rimraf": "2.6.2", + "signal-exit": "3.0.2", + "spawn-wrap": "1.4.2", + "test-exclude": "4.2.1", + "yargs": "11.1.0", + "yargs-parser": "8.1.0" + }, + "dependencies": { + "align-text": { + "version": "0.1.4", + "bundled": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "bundled": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "ansi-styles": { + "version": "2.2.1", + "bundled": true + }, + "append-transform": { + "version": "0.4.0", + "bundled": true, + "requires": { + "default-require-extensions": "1.0.0" + } + }, + "archy": { + "version": "1.0.0", + "bundled": true + }, + "arr-diff": { + "version": "4.0.0", + "bundled": true + }, + "arr-flatten": { + "version": "1.1.0", + "bundled": true + }, + "arr-union": { + "version": "3.1.0", + "bundled": true + }, + "array-unique": { + "version": "0.3.2", + "bundled": true + }, + "arrify": { + "version": "1.0.1", + "bundled": true + }, + "assign-symbols": { + "version": "1.0.0", + "bundled": true + }, + "async": { + "version": "1.5.2", + "bundled": true + }, + "atob": { + "version": "2.1.1", + "bundled": true + }, + "babel-code-frame": { + "version": "6.26.0", + "bundled": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-generator": { + "version": "6.26.1", + "bundled": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.10", + "source-map": "0.5.7", + "trim-right": "1.0.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "bundled": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "bundled": true, + "requires": { + "core-js": "2.5.6", + "regenerator-runtime": "0.11.1" + } + }, + "babel-template": { + "version": "6.26.0", + "bundled": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.10" + } + }, + "babel-traverse": { + "version": "6.26.0", + "bundled": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.4", + "lodash": "4.17.10" + } + }, + "babel-types": { + "version": "6.26.0", + "bundled": true, + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.10", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "bundled": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "base": { + "version": "0.11.2", + "bundled": true, + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "bundled": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "builtin-modules": { + "version": "1.1.1", + "bundled": true + }, + "cache-base": { + "version": "1.0.1", + "bundled": true, + "requires": { + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "caching-transform": { + "version": "1.0.1", + "bundled": true, + "requires": { + "md5-hex": "1.3.0", + "mkdirp": "0.5.1", + "write-file-atomic": "1.3.4" + } + }, + "camelcase": { + "version": "1.2.1", + "bundled": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "bundled": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "1.1.3", + "bundled": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "class-utils": { + "version": "0.3.6", + "bundled": true, + "requires": { + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "cliui": { + "version": "2.1.0", + "bundled": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "bundled": true, + "optional": true + } + } + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "collection-visit": { + "version": "1.0.0", + "bundled": true, + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "commondir": { + "version": "1.0.1", + "bundled": true + }, + "component-emitter": { + "version": "1.2.1", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "convert-source-map": { + "version": "1.5.1", + "bundled": true + }, + "copy-descriptor": { + "version": "0.1.1", + "bundled": true + }, + "core-js": { + "version": "2.5.6", + "bundled": true + }, + "cross-spawn": { + "version": "4.0.2", + "bundled": true, + "requires": { + "lru-cache": "4.1.3", + "which": "1.3.0" + } + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "requires": { + "ms": "2.0.0" + } + }, + "debug-log": { + "version": "1.0.1", + "bundled": true + }, + "decamelize": { + "version": "1.2.0", + "bundled": true + }, + "decode-uri-component": { + "version": "0.2.0", + "bundled": true + }, + "default-require-extensions": { + "version": "1.0.0", + "bundled": true, + "requires": { + "strip-bom": "2.0.0" + } + }, + "define-property": { + "version": "2.0.2", + "bundled": true, + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "detect-indent": { + "version": "4.0.0", + "bundled": true, + "requires": { + "repeating": "2.0.1" + } + }, + "error-ex": { + "version": "1.3.1", + "bundled": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "bundled": true + }, + "esutils": { + "version": "2.0.2", + "bundled": true + }, + "execa": { + "version": "0.7.0", + "bundled": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "bundled": true, + "requires": { + "lru-cache": "4.1.3", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "bundled": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "bundled": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "bundled": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "bundled": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "fill-range": { + "version": "4.0.0", + "bundled": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "find-cache-dir": { + "version": "0.1.1", + "bundled": true, + "requires": { + "commondir": "1.0.1", + "mkdirp": "0.5.1", + "pkg-dir": "1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "bundled": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "bundled": true + }, + "foreground-child": { + "version": "1.5.6", + "bundled": true, + "requires": { + "cross-spawn": "4.0.2", + "signal-exit": "3.0.2" + } + }, + "fragment-cache": { + "version": "0.2.1", + "bundled": true, + "requires": { + "map-cache": "0.2.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "get-caller-file": { + "version": "1.0.2", + "bundled": true + }, + "get-stream": { + "version": "3.0.0", + "bundled": true + }, + "get-value": { + "version": "2.0.6", + "bundled": true + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "globals": { + "version": "9.18.0", + "bundled": true + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true + }, + "handlebars": { + "version": "4.0.11", + "bundled": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "bundled": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "has-ansi": { + "version": "2.0.0", + "bundled": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "1.0.0", + "bundled": true + }, + "has-value": { + "version": "1.0.0", + "bundled": true, + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "has-values": { + "version": "1.0.0", + "bundled": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "bundled": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "bundled": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "hosted-git-info": { + "version": "2.6.0", + "bundled": true + }, + "imurmurhash": { + "version": "0.1.4", + "bundled": true + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "invariant": { + "version": "2.2.4", + "bundled": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "bundled": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "bundled": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-arrayish": { + "version": "0.2.1", + "bundled": true + }, + "is-buffer": { + "version": "1.1.6", + "bundled": true + }, + "is-builtin-module": { + "version": "1.0.0", + "bundled": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "bundled": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "bundled": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "bundled": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "bundled": true + }, + "is-finite": { + "version": "1.0.2", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true + }, + "is-number": { + "version": "3.0.0", + "bundled": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-odd": { + "version": "2.0.0", + "bundled": true, + "requires": { + "is-number": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "bundled": true + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "bundled": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "is-stream": { + "version": "1.1.0", + "bundled": true + }, + "is-utf8": { + "version": "0.2.1", + "bundled": true + }, + "is-windows": { + "version": "1.0.2", + "bundled": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true + }, + "isobject": { + "version": "3.0.1", + "bundled": true + }, + "istanbul-lib-coverage": { + "version": "1.2.0", + "bundled": true + }, + "istanbul-lib-hook": { + "version": "1.1.0", + "bundled": true, + "requires": { + "append-transform": "0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.10.1", + "bundled": true, + "requires": { + "babel-generator": "6.26.1", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "istanbul-lib-coverage": "1.2.0", + "semver": "5.5.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.3", + "bundled": true, + "requires": { + "istanbul-lib-coverage": "1.2.0", + "mkdirp": "0.5.1", + "path-parse": "1.0.5", + "supports-color": "3.2.3" + }, + "dependencies": { + "supports-color": { + "version": "3.2.3", + "bundled": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.3", + "bundled": true, + "requires": { + "debug": "3.1.0", + "istanbul-lib-coverage": "1.2.0", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "source-map": "0.5.7" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "bundled": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "istanbul-reports": { + "version": "1.4.0", + "bundled": true, + "requires": { + "handlebars": "4.0.11" + } + }, + "js-tokens": { + "version": "3.0.2", + "bundled": true + }, + "jsesc": { + "version": "1.3.0", + "bundled": true + }, + "kind-of": { + "version": "3.2.2", + "bundled": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "lazy-cache": { + "version": "1.0.4", + "bundled": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "bundled": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "bundled": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "bundled": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "bundled": true + } + } + }, + "lodash": { + "version": "4.17.10", + "bundled": true + }, + "longest": { + "version": "1.0.1", + "bundled": true + }, + "loose-envify": { + "version": "1.3.1", + "bundled": true, + "requires": { + "js-tokens": "3.0.2" + } + }, + "lru-cache": { + "version": "4.1.3", + "bundled": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "map-cache": { + "version": "0.2.2", + "bundled": true + }, + "map-visit": { + "version": "1.0.0", + "bundled": true, + "requires": { + "object-visit": "1.0.1" + } + }, + "md5-hex": { + "version": "1.3.0", + "bundled": true, + "requires": { + "md5-o-matic": "0.1.1" + } + }, + "md5-o-matic": { + "version": "0.1.1", + "bundled": true + }, + "mem": { + "version": "1.1.0", + "bundled": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "merge-source-map": { + "version": "1.1.0", + "bundled": true, + "requires": { + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "bundled": true + } + } + }, + "micromatch": { + "version": "3.1.10", + "bundled": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "mimic-fn": { + "version": "1.2.0", + "bundled": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "mixin-deep": { + "version": "1.3.1", + "bundled": true, + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "bundled": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true + }, + "nanomatch": { + "version": "1.2.9", + "bundled": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "fragment-cache": "0.2.1", + "is-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "bundled": true + }, + "array-unique": { + "version": "0.3.2", + "bundled": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "normalize-package-data": { + "version": "2.4.0", + "bundled": true, + "requires": { + "hosted-git-info": "2.6.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.3" + } + }, + "npm-run-path": { + "version": "2.0.2", + "bundled": true, + "requires": { + "path-key": "2.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "object-copy": { + "version": "0.1.0", + "bundled": true, + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "bundled": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "object.pick": { + "version": "1.3.0", + "bundled": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "optimist": { + "version": "0.6.1", + "bundled": true, + "requires": { + "minimist": "0.0.8", + "wordwrap": "0.0.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-locale": { + "version": "2.1.0", + "bundled": true, + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "bundled": true + }, + "p-limit": { + "version": "1.2.0", + "bundled": true, + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "bundled": true, + "requires": { + "p-limit": "1.2.0" + } + }, + "p-try": { + "version": "1.0.0", + "bundled": true + }, + "parse-json": { + "version": "2.2.0", + "bundled": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "pascalcase": { + "version": "0.1.1", + "bundled": true + }, + "path-exists": { + "version": "2.1.0", + "bundled": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "path-key": { + "version": "2.0.1", + "bundled": true + }, + "path-parse": { + "version": "1.0.5", + "bundled": true + }, + "path-type": { + "version": "1.1.0", + "bundled": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pify": { + "version": "2.3.0", + "bundled": true + }, + "pinkie": { + "version": "2.0.4", + "bundled": true + }, + "pinkie-promise": { + "version": "2.0.1", + "bundled": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "1.0.0", + "bundled": true, + "requires": { + "find-up": "1.1.2" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "bundled": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "bundled": true + }, + "pseudomap": { + "version": "1.0.2", + "bundled": true + }, + "read-pkg": { + "version": "1.1.0", + "bundled": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "bundled": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "bundled": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + } + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "bundled": true + }, + "regex-not": { + "version": "1.0.2", + "bundled": true, + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + } + }, + "repeat-element": { + "version": "1.1.2", + "bundled": true + }, + "repeat-string": { + "version": "1.6.1", + "bundled": true + }, + "repeating": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "require-directory": { + "version": "2.1.1", + "bundled": true + }, + "require-main-filename": { + "version": "1.0.1", + "bundled": true + }, + "resolve-from": { + "version": "2.0.0", + "bundled": true + }, + "resolve-url": { + "version": "0.2.1", + "bundled": true + }, + "ret": { + "version": "0.1.15", + "bundled": true + }, + "right-align": { + "version": "0.1.3", + "bundled": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-regex": { + "version": "1.1.0", + "bundled": true, + "requires": { + "ret": "0.1.15" + } + }, + "semver": { + "version": "5.5.0", + "bundled": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "set-value": { + "version": "2.0.0", + "bundled": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "bundled": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "slide": { + "version": "1.1.6", + "bundled": true + }, + "snapdragon": { + "version": "0.8.2", + "bundled": true, + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.1", + "use": "3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "bundled": true, + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "bundled": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "source-map": { + "version": "0.5.7", + "bundled": true + }, + "source-map-resolve": { + "version": "0.5.1", + "bundled": true, + "requires": { + "atob": "2.1.1", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "bundled": true + }, + "spawn-wrap": { + "version": "1.4.2", + "bundled": true, + "requires": { + "foreground-child": "1.5.6", + "mkdirp": "0.5.1", + "os-homedir": "1.0.2", + "rimraf": "2.6.2", + "signal-exit": "3.0.2", + "which": "1.3.0" + } + }, + "spdx-correct": { + "version": "3.0.0", + "bundled": true, + "requires": { + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "bundled": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "bundled": true, + "requires": { + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "bundled": true + }, + "split-string": { + "version": "3.1.0", + "bundled": true, + "requires": { + "extend-shallow": "3.0.2" + } + }, + "static-extend": { + "version": "0.1.2", + "bundled": true, + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "string-width": { + "version": "2.1.1", + "bundled": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "bundled": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "bundled": true + }, + "supports-color": { + "version": "2.0.0", + "bundled": true + }, + "test-exclude": { + "version": "4.2.1", + "bundled": true, + "requires": { + "arrify": "1.0.1", + "micromatch": "3.1.10", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "require-main-filename": "1.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "bundled": true + }, + "array-unique": { + "version": "0.3.2", + "bundled": true + }, + "braces": { + "version": "2.3.2", + "bundled": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "bundled": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "bundled": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "bundled": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "bundled": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "bundled": true + } + } + }, + "extglob": { + "version": "2.0.4", + "bundled": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "bundled": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "bundled": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true + }, + "micromatch": { + "version": "3.1.10", + "bundled": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + } + } + }, + "to-fast-properties": { + "version": "1.0.3", + "bundled": true + }, + "to-object-path": { + "version": "0.3.0", + "bundled": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "to-regex": { + "version": "3.0.2", + "bundled": true, + "requires": { + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "bundled": true, + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "bundled": true, + "requires": { + "kind-of": "3.2.2" + } + } + } + }, + "trim-right": { + "version": "1.0.1", + "bundled": true + }, + "uglify-js": { + "version": "2.8.29", + "bundled": true, + "optional": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.10.0", + "bundled": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "union-value": { + "version": "1.0.0", + "bundled": true, + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "bundled": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "unset-value": { + "version": "1.0.0", + "bundled": true, + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "bundled": true, + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "bundled": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "bundled": true + }, + "isobject": { + "version": "3.0.1", + "bundled": true + } + } + }, + "urix": { + "version": "0.1.0", + "bundled": true + }, + "use": { + "version": "3.1.0", + "bundled": true, + "requires": { + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "bundled": true + } + } + }, + "validate-npm-package-license": { + "version": "3.0.3", + "bundled": true, + "requires": { + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" + } + }, + "which": { + "version": "1.3.0", + "bundled": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "bundled": true + }, + "window-size": { + "version": "0.1.0", + "bundled": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.3", + "bundled": true + }, + "wrap-ansi": { + "version": "2.1.0", + "bundled": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "write-file-atomic": { + "version": "1.3.4", + "bundled": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "slide": "1.1.6" + } + }, + "y18n": { + "version": "3.2.1", + "bundled": true + }, + "yallist": { + "version": "2.1.2", + "bundled": true + }, + "yargs": { + "version": "11.1.0", + "bundled": true, + "requires": { + "cliui": "4.1.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "9.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true + }, + "camelcase": { + "version": "4.1.0", + "bundled": true + }, + "cliui": { + "version": "4.1.0", + "bundled": true, + "requires": { + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "wrap-ansi": "2.1.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "requires": { + "ansi-regex": "3.0.0" + } + }, + "yargs-parser": { + "version": "9.0.2", + "bundled": true, + "requires": { + "camelcase": "4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "8.1.0", + "bundled": true, + "requires": { + "camelcase": "4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "bundled": true + } + } + } + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "opener": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz", + "integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=" + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "own-or": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/own-or/-/own-or-1.0.0.tgz", + "integrity": "sha1-Tod/vtqaLsgAD7wLyuOWRe6L+Nw=" + }, + "own-or-env": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-or-env/-/own-or-env-1.0.1.tgz", + "integrity": "sha512-y8qULRbRAlL6x2+M0vIe7jJbJx/kmUTzYonRAa2ayesR2qWLswninkVyeJe4x3IEXhdgoNodzjQRKAoEs6Fmrw==", + "requires": { + "own-or": "1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "2.0.4" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", + "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.79.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.7.0", + "caseless": "0.11.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "2.0.6", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "qs": "6.3.2", + "stringstream": "0.0.6", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.4.3", + "uuid": "3.2.1" + } + }, + "resolve": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "requires": { + "path-parse": "1.0.5" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "2.16.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", + "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "stringstream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "tap": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/tap/-/tap-10.7.3.tgz", + "integrity": "sha512-oS/FIq+tcmxVgYn5usKtLsX+sOHNEj+G7JIQE9SBjO5mVYB1rbaEJJiDbnYp8k0ZqY2Pe4HbYEpkvzm9jfLDyw==", + "requires": { + "bind-obj-methods": "1.0.0", + "bluebird": "3.5.1", + "clean-yaml-object": "0.1.0", + "color-support": "1.1.3", + "coveralls": "2.13.3", + "foreground-child": "1.5.6", + "fs-exists-cached": "1.0.0", + "function-loop": "1.0.1", + "glob": "7.1.2", + "isexe": "2.0.0", + "js-yaml": "3.11.0", + "nyc": "11.8.0", + "opener": "1.4.3", + "os-homedir": "1.0.2", + "own-or": "1.0.0", + "own-or-env": "1.0.1", + "readable-stream": "2.3.6", + "signal-exit": "3.0.2", + "source-map-support": "0.4.18", + "stack-utils": "1.0.1", + "tap-mocha-reporter": "3.0.7", + "tap-parser": "5.4.0", + "tmatch": "3.1.0", + "trivial-deferred": "1.0.1", + "tsame": "1.1.2", + "yapool": "1.0.0" + }, + "dependencies": { + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "requires": { + "source-map": "0.5.7" + } + } + } + }, + "tap-mocha-reporter": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/tap-mocha-reporter/-/tap-mocha-reporter-3.0.7.tgz", + "integrity": "sha512-GHVXJ38C3oPRpM3YUc43JlGdpVZYiKeT1fmAd3HH2+J+ZWwsNAUFvRRdoGsXLw9+gU9o+zXpBqhS/oXyRQYwlA==", + "requires": { + "color-support": "1.1.3", + "debug": "2.6.9", + "diff": "1.4.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "js-yaml": "3.11.0", + "readable-stream": "2.3.6", + "tap-parser": "5.4.0", + "unicode-length": "1.0.3" + } + }, + "tap-parser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-5.4.0.tgz", + "integrity": "sha512-BIsIaGqv7uTQgTW1KLTMNPSEQf4zDDPgYOBRdgOfuB+JFOLRBfEu6cLa/KvMvmqggu1FKXDfitjLwsq4827RvA==", + "requires": { + "events-to-array": "1.1.2", + "js-yaml": "3.11.0", + "readable-stream": "2.3.6" + } + }, + "tmatch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-3.1.0.tgz", + "integrity": "sha512-W3MSATOCN4pVu2qFxmJLIArSifeSOFqnfx9hiUaVgOmeRoI2NbU7RNga+6G+L8ojlFeQge+ZPCclWyUpQ8UeNQ==" + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "1.4.1" + } + }, + "trivial-deferred": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trivial-deferred/-/trivial-deferred-1.0.1.tgz", + "integrity": "sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM=" + }, + "tsame": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tsame/-/tsame-1.1.2.tgz", + "integrity": "sha512-ovCs24PGjmByVPr9tSIOs/yjUX9sJl0grEmOsj9dZA/UknQkgPOKcUqM84aSCvt9awHuhc/boMzTg3BHFalxWw==" + }, + "tslib": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.1.tgz", + "integrity": "sha512-avfPS28HmGLLc2o4elcc2EIq2FcH++Yo5YxpBZi9Yw93BCTGFthI4HPE4Rpep6vSYQaK8e69PelM44tPj+RaQg==" + }, + "tslint": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.10.0.tgz", + "integrity": "sha1-EeJrzLiK+gLdDZlWyuPUVAtfVMM=", + "requires": { + "babel-code-frame": "6.26.0", + "builtin-modules": "1.1.1", + "chalk": "2.4.1", + "commander": "2.15.1", + "diff": "3.5.0", + "glob": "7.1.2", + "js-yaml": "3.11.0", + "minimatch": "3.0.4", + "resolve": "1.7.1", + "semver": "5.5.0", + "tslib": "1.9.1", + "tsutils": "2.27.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "tsutils": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz", + "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==", + "requires": { + "tslib": "1.9.1" + } + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "unicode-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/unicode-length/-/unicode-length-1.0.3.tgz", + "integrity": "sha1-Wtp6f+1RhBpBijKM8UlHisg1irs=", + "requires": { + "punycode": "1.4.1", + "strip-ansi": "3.0.1" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yapool": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yapool/-/yapool-1.0.0.tgz", + "integrity": "sha1-9pPymjFbUNmp2iZGp6ZkXJaYW2o=" + } + } +} diff --git a/packages/aws-cdk-ec2/package.json b/packages/aws-cdk-ec2/package.json new file mode 100644 index 0000000000000..8413a40317983 --- /dev/null +++ b/packages/aws-cdk-ec2/package.json @@ -0,0 +1,51 @@ +{ + "name": "aws-cdk-ec2", + "version": "0.6.0", + "description": "CDK Constructs for AWS EC2", + "main": "lib/index.js", + "types": "lib/index.ts", + "jsii": { + "outdir": "dist", + "bundledDependencies": [ + "aws-cdk-util" + ], + "names": { + "java": "com.amazonaws.cdk.ec2", + "dotnet": "Aws.Cdk.Ec2" + } + }, + "repository": { + "type": "git", + "url": "git://github.com/awslabs/aws-cdk" + }, + "scripts": { + "prepare": "jsii && tslint -p . && pkglint", + "watch": "jsii -w", + "lint": "tsc && tslint -p . --force", + "test": "nodeunit test/test.*.js && cdk-integ-assert", + "integ": "cdk-integ", + "pkglint": "pkglint -f" + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "ec2" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "license": "Apache-2.0", + "devDependencies": { + "aws-cdk-assert": "^0.6.0", + "aws-cdk-toolkit": "^0.6.0", + "pkglint": "^0.6.0" + }, + "dependencies": { + "aws-cdk": "^0.6.0", + "aws-cdk-iam": "^0.6.0", + "aws-cdk-resources": "^0.6.0", + "aws-cdk-util": "^0.6.0" + } +} diff --git a/packages/aws-cdk-ec2/test/demo.split-stack-vpc.ts b/packages/aws-cdk-ec2/test/demo.split-stack-vpc.ts new file mode 100644 index 0000000000000..4ef2d4f36b49b --- /dev/null +++ b/packages/aws-cdk-ec2/test/demo.split-stack-vpc.ts @@ -0,0 +1,40 @@ +#!/usr/bin/env node +// Like an integ test, but our integ test doesn't currently +// support multi-stack deployments since we have no good way of +// ordering stack deployments. So run this test by hand for now +// until we have that. +import { App, Stack } from 'aws-cdk'; +import { AmazonLinuxImage, AnyIPv4, ClassicLoadBalancer, Fleet, InstanceClass, InstanceSize, + InstanceTypePair, VpcNetwork, VpcNetworkRef } from '../lib'; + +const app = new App(process.argv); +const vpcStack = new Stack(app, 'VPCStack'); + +const exportedVpc = new VpcNetwork(vpcStack, 'VPC', { + maxAZs: 3 +}); + +const appStack = new Stack(app, 'AppStack'); + +const importedVpc = VpcNetworkRef.import(appStack, 'VPC', exportedVpc.export()); + +const fleet = new Fleet(appStack, 'Fleet', { + vpc: importedVpc, + instanceType: new InstanceTypePair(InstanceClass.Burstable2, InstanceSize.Micro), + machineImage: new AmazonLinuxImage() +}); + +new ClassicLoadBalancer(appStack, 'LB', { + vpc: importedVpc, + internetFacing: true, + listeners: [{ + externalPort: 80, + allowConnectionsFrom: [new AnyIPv4()] + }], + healthCheck: { + port: 80 + }, + targets: [fleet] +}); + +process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/aws-cdk-ec2/test/integ.everything.expected.json b/packages/aws-cdk-ec2/test/integ.everything.expected.json new file mode 100644 index 0000000000000..08d1074802b83 --- /dev/null +++ b/packages/aws-cdk-ec2/test/integ.everything.expected.json @@ -0,0 +1,538 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "test-region-1a", + "CidrBlock": "10.0.0.0/19", + "MapPublicIpOnLaunch": true, + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPublicSubnet1RouteTableAssociatioin249B4093": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + }, + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + } + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "test-region-1a", + "CidrBlock": "10.0.32.0/19", + "MapPublicIpOnLaunch": false, + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPrivateSubnet1RouteTableAssociatioin77F7CA18": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + }, + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + } + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "test-region-1b", + "CidrBlock": "10.0.64.0/19", + "MapPublicIpOnLaunch": true, + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPublicSubnet2RouteTableAssociatioin766225D7": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + }, + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "test-region-1b", + "CidrBlock": "10.0.96.0/19", + "MapPublicIpOnLaunch": false, + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPrivateSubnet2RouteTableAssociatioinC31995B4": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + }, + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + } + } + }, + "VPCPublicSubnet3Subnet631C5E25": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "test-region-1c", + "CidrBlock": "10.0.128.0/19", + "MapPublicIpOnLaunch": true, + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPublicSubnet3RouteTable98AE0E14": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPublicSubnet3RouteTableAssociatioinF4E24B3B": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + } + } + }, + "VPCPublicSubnet3EIPAD4BC883": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet3NATGatewayD3048F5C": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet3EIPAD4BC883", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + } + } + }, + "VPCPublicSubnet3DefaultRouteA0D29D46": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + }, + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + } + } + }, + "VPCPrivateSubnet3Subnet3EDCD457": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "test-region-1c", + "CidrBlock": "10.0.160.0/19", + "MapPublicIpOnLaunch": false, + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPrivateSubnet3RouteTable192186F8": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "VPCPrivateSubnet3RouteTableAssociatioin3B0B6B38": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + } + }, + "VPCPrivateSubnet3DefaultRoute27F311AE": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet3NATGatewayD3048F5C" + }, + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway" + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + }, + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "FleetInstanceSecurityGroupA8C3D7AD": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-cdk-ec2-integ/Fleet/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Outbound traffic allowed by default", + "FromPort": -1, + "IpProtocol": "-1", + "ToPort": -1 + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "FleetInstanceSecurityGroupPort80LBtofleetDC12B17A": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "Description": "Port 80 LB to fleet", + "FromPort": 80, + "GroupId": { + "Fn::GetAtt": [ + "FleetInstanceSecurityGroupA8C3D7AD", + "GroupId" + ] + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + }, + "ToPort": 80 + } + }, + "FleetInstanceRoleA605DB82": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "FleetInstanceProfileC6192A66": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "FleetInstanceRoleA605DB82" + } + ] + } + }, + "FleetLaunchConfig59F79D36": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "IamInstanceProfile": { + "Ref": "FleetInstanceProfileC6192A66" + }, + "ImageId": "ami-1234", + "InstanceType": "t2.micro", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FleetInstanceSecurityGroupA8C3D7AD", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": "#!/bin/bash\n" + } + }, + "DependsOn": [ + "FleetInstanceRoleA605DB82" + ] + }, + "FleetASG3971DFE5": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "FleetLaunchConfig59F79D36" + }, + "LoadBalancerNames": [ + { + "Ref": "LB8A12904C" + } + ], + "MaxSize": "1", + "MinSize": "1", + "VPCZoneIdentifier": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ] + } + }, + "LBSecurityGroup8A41EA2B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-cdk-ec2-integ/LB/SecurityGroup", + "SecurityGroupEgress": [], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Default rule allow on 80", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "LBSecurityGroupPort80LBtofleet0986F2E8": { + "Type": "AWS::EC2::SecurityGroupEgress", + "Properties": { + "Description": "Port 80 LB to fleet", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "FleetInstanceSecurityGroupA8C3D7AD", + "GroupId" + ] + }, + "FromPort": 80, + "GroupId": { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + }, + "IpProtocol": "tcp", + "ToPort": 80 + } + }, + "LB8A12904C": { + "Type": "AWS::ElasticLoadBalancing::LoadBalancer", + "Properties": { + "HealthCheck": { + "HealthyThreshold": "2", + "Interval": "30", + "Target": "HTTP:80/", + "Timeout": "5", + "UnhealthyThreshold": "5" + }, + "Listeners": [ + { + "InstancePort": "80", + "InstanceProtocol": "http", + "LoadBalancerPort": "80", + "Protocol": "http" + } + ], + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-ec2/test/integ.everything.ts b/packages/aws-cdk-ec2/test/integ.everything.ts new file mode 100644 index 0000000000000..89d7b90cc9a93 --- /dev/null +++ b/packages/aws-cdk-ec2/test/integ.everything.ts @@ -0,0 +1,32 @@ +#!/usr/bin/env node +import { App, Stack } from 'aws-cdk'; +import { AmazonLinuxImage, AnyIPv4, ClassicLoadBalancer, Fleet, InstanceClass, + InstanceSize, InstanceTypePair, VpcNetwork } from '../lib'; + +const app = new App(process.argv); +const stack = new Stack(app, 'aws-cdk-ec2-integ'); + +const vpc = new VpcNetwork(stack, 'VPC', { + maxAZs: 3 +}); + +const fleet = new Fleet(stack, 'Fleet', { + vpc, + instanceType: new InstanceTypePair(InstanceClass.Burstable2, InstanceSize.Micro), + machineImage: new AmazonLinuxImage(), +}); + +new ClassicLoadBalancer(stack, 'LB', { + vpc, + internetFacing: true, + listeners: [{ + externalPort: 80, + allowConnectionsFrom: [new AnyIPv4()] + }], + healthCheck: { + port: 80 + }, + targets: [fleet] +}); + +process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/aws-cdk-ec2/test/integ.vpc.expected.json b/packages/aws-cdk-ec2/test/integ.vpc.expected.json new file mode 100644 index 0000000000000..c5b9cca0ebd0b --- /dev/null +++ b/packages/aws-cdk-ec2/test/integ.vpc.expected.json @@ -0,0 +1,340 @@ +{ + "Resources": { + "MyVpcF9F0CA6F": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [] + } + }, + "MyVpcPublicSubnet1SubnetF6608456": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "test-region-1a", + "CidrBlock": "10.0.0.0/19", + "MapPublicIpOnLaunch": true, + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + }, + "MyVpcPublicSubnet1RouteTableC46AB2F4": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + }, + "MyVpcPublicSubnet1RouteTableAssociatioin3562612E": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet1RouteTableC46AB2F4" + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet1SubnetF6608456" + } + } + }, + "MyVpcPublicSubnet1EIP096967CB": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "MyVpcPublicSubnet1NATGatewayAD3400C1": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "MyVpcPublicSubnet1EIP096967CB", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet1SubnetF6608456" + } + } + }, + "MyVpcPublicSubnet1DefaultRoute95FDF9EB": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "MyVpcIGW5C4A4F63" + }, + "RouteTableId": { + "Ref": "MyVpcPublicSubnet1RouteTableC46AB2F4" + } + } + }, + "MyVpcPrivateSubnet1Subnet5057CF7E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "test-region-1a", + "CidrBlock": "10.0.32.0/19", + "MapPublicIpOnLaunch": false, + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + }, + "MyVpcPrivateSubnet1RouteTable8819E6E2": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + }, + "MyVpcPrivateSubnet1RouteTableAssociatioin90CF6BAB": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet1RouteTable8819E6E2" + }, + "SubnetId": { + "Ref": "MyVpcPrivateSubnet1Subnet5057CF7E" + } + } + }, + "MyVpcPrivateSubnet1DefaultRouteA8CDE2FA": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "MyVpcPublicSubnet1NATGatewayAD3400C1" + }, + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet1RouteTable8819E6E2" + } + } + }, + "MyVpcPublicSubnet2Subnet492B6BFB": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "test-region-1b", + "CidrBlock": "10.0.64.0/19", + "MapPublicIpOnLaunch": true, + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + }, + "MyVpcPublicSubnet2RouteTable1DF17386": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + }, + "MyVpcPublicSubnet2RouteTableAssociatioin8E74FB35": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet2RouteTable1DF17386" + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet2Subnet492B6BFB" + } + } + }, + "MyVpcPublicSubnet2EIP8CCBA239": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "MyVpcPublicSubnet2NATGateway91BFBEC9": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "MyVpcPublicSubnet2EIP8CCBA239", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet2Subnet492B6BFB" + } + } + }, + "MyVpcPublicSubnet2DefaultRoute052936F6": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "MyVpcIGW5C4A4F63" + }, + "RouteTableId": { + "Ref": "MyVpcPublicSubnet2RouteTable1DF17386" + } + } + }, + "MyVpcPrivateSubnet2Subnet0040C983": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "test-region-1b", + "CidrBlock": "10.0.96.0/19", + "MapPublicIpOnLaunch": false, + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + }, + "MyVpcPrivateSubnet2RouteTableCEDCEECE": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + }, + "MyVpcPrivateSubnet2RouteTableAssociatioin803693C0": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet2RouteTableCEDCEECE" + }, + "SubnetId": { + "Ref": "MyVpcPrivateSubnet2Subnet0040C983" + } + } + }, + "MyVpcPrivateSubnet2DefaultRoute9CE96294": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "MyVpcPublicSubnet2NATGateway91BFBEC9" + }, + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet2RouteTableCEDCEECE" + } + } + }, + "MyVpcPublicSubnet3Subnet57EEE236": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "test-region-1c", + "CidrBlock": "10.0.128.0/19", + "MapPublicIpOnLaunch": true, + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + }, + "MyVpcPublicSubnet3RouteTable15028F08": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + }, + "MyVpcPublicSubnet3RouteTableAssociatioinA3FD1B71": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPublicSubnet3RouteTable15028F08" + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet3Subnet57EEE236" + } + } + }, + "MyVpcPublicSubnet3EIPC5ACADAB": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "MyVpcPublicSubnet3NATGatewayD4B50EBE": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "MyVpcPublicSubnet3EIPC5ACADAB", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "MyVpcPublicSubnet3Subnet57EEE236" + } + } + }, + "MyVpcPublicSubnet3DefaultRoute3A83AB36": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "MyVpcIGW5C4A4F63" + }, + "RouteTableId": { + "Ref": "MyVpcPublicSubnet3RouteTable15028F08" + } + } + }, + "MyVpcPrivateSubnet3Subnet772D6AD7": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": "test-region-1c", + "CidrBlock": "10.0.160.0/19", + "MapPublicIpOnLaunch": false, + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + }, + "MyVpcPrivateSubnet3RouteTableB790927C": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + }, + "MyVpcPrivateSubnet3RouteTableAssociatioinFB4A6FE6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet3RouteTableB790927C" + }, + "SubnetId": { + "Ref": "MyVpcPrivateSubnet3Subnet772D6AD7" + } + } + }, + "MyVpcPrivateSubnet3DefaultRouteEC11C0C5": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "MyVpcPublicSubnet3NATGatewayD4B50EBE" + }, + "RouteTableId": { + "Ref": "MyVpcPrivateSubnet3RouteTableB790927C" + } + } + }, + "MyVpcIGW5C4A4F63": { + "Type": "AWS::EC2::InternetGateway" + }, + "MyVpcVPCGW488ACE0D": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "InternetGatewayId": { + "Ref": "MyVpcIGW5C4A4F63" + }, + "VpcId": { + "Ref": "MyVpcF9F0CA6F" + } + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-ec2/test/integ.vpc.ts b/packages/aws-cdk-ec2/test/integ.vpc.ts new file mode 100644 index 0000000000000..5595fc5494668 --- /dev/null +++ b/packages/aws-cdk-ec2/test/integ.vpc.ts @@ -0,0 +1,12 @@ +import { App, Stack } from 'aws-cdk'; +import { OutboundTrafficMode, VpcNetwork } from '..'; + +const app = new App(process.argv); + +const stack = new Stack(app, 'aws-cdk-ec2-vpc'); + +new VpcNetwork(stack, 'MyVpc', { + outboundTraffic: OutboundTrafficMode.FromPublicAndPrivateSubnets +}); + +process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/aws-cdk-ec2/test/test.connections.ts b/packages/aws-cdk-ec2/test/test.connections.ts new file mode 100644 index 0000000000000..6b3c4f4b9ae4d --- /dev/null +++ b/packages/aws-cdk-ec2/test/test.connections.ts @@ -0,0 +1,28 @@ +import { Stack } from 'aws-cdk'; +import { Test } from 'nodeunit'; +import { Connections, IConnectable, IConnections, SecurityGroup, TcpPort, VpcNetwork } from '../lib'; + +export = { + 'peering between two security groups does not recursive infinitely'(test: Test) { + // GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' }}); + + const vpc = new VpcNetwork(stack, 'VPC'); + const sg1 = new SecurityGroup(stack, 'SG1', { vpc }); + const sg2 = new SecurityGroup(stack, 'SG2', { vpc }); + + const conn1 = new ConnectionsHolder(new Connections(sg1)); + const conn2 = new ConnectionsHolder(new Connections(sg2)); + + // WHEN + conn1.connections.allowTo(conn2, new TcpPort(80), 'Test'); + + // THEN + test.done(); + } +}; + +class ConnectionsHolder implements IConnectable { + constructor(public readonly connections: IConnections) { + } +} diff --git a/packages/aws-cdk-ec2/test/test.fleet.ts b/packages/aws-cdk-ec2/test/test.fleet.ts new file mode 100644 index 0000000000000..2bf70eaade5ec --- /dev/null +++ b/packages/aws-cdk-ec2/test/test.fleet.ts @@ -0,0 +1,245 @@ +import { PolicyStatement, Stack } from 'aws-cdk'; +import { expect } from 'aws-cdk-assert'; +import { Test } from 'nodeunit'; +import { AmazonLinuxImage, Fleet, InstanceClass, InstanceSize, InstanceTypePair, VpcNetwork, VpcNetworkId, VpcSubnetId } from '../lib'; + +// tslint:disable:object-literal-key-quotes + +export = { + 'default fleet'(test: Test) { + const stack = new Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + const vpc = mockVpc(stack); + + new Fleet(stack, 'MyFleet', { + instanceType: new InstanceTypePair(InstanceClass.M4, InstanceSize.Micro), + machineImage: new AmazonLinuxImage(), + vpc + }); + + expect(stack).toMatch({ + "Resources": { + "MyFleetInstanceSecurityGroup774E8234": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "MyFleet/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Outbound traffic allowed by default", + "FromPort": -1, + "IpProtocol": "-1", + "ToPort": -1 + } + ], + "SecurityGroupIngress": [], + "VpcId": "my-vpc" + } + }, + "MyFleetInstanceRole25A84AB8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyFleetInstanceProfile70A58496": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "MyFleetInstanceRole25A84AB8" + } + ] + } + }, + "MyFleetLaunchConfig5D7F9801": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "IamInstanceProfile": { + "Ref": "MyFleetInstanceProfile70A58496" + }, + "ImageId": "", + "InstanceType": "m4.micro", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "MyFleetInstanceSecurityGroup774E8234", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": "#!/bin/bash\n" + } + }, + "DependsOn": [ + "MyFleetInstanceRole25A84AB8" + ] + }, + "MyFleetASG88E55886": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "MyFleetLaunchConfig5D7F9801" + }, + "LoadBalancerNames": [], + "MaxSize": "1", + "MinSize": "1", + "VPCZoneIdentifier": [ + "pri1" + ] + } + } + } + }); + + test.done(); + }, + + 'addToRolePolicy can be used to add statements to the role policy'(test: Test) { + const stack = new Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + const vpc = mockVpc(stack); + + const fleet = new Fleet(stack, 'MyFleet', { + instanceType: new InstanceTypePair(InstanceClass.M4, InstanceSize.Micro), + machineImage: new AmazonLinuxImage(), + vpc + }); + + fleet.addToRolePolicy(new PolicyStatement() + .addAction('*') + .addResource('*')); + + expect(stack).toMatch({ + "Resources": { + "MyFleetInstanceSecurityGroup774E8234": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "MyFleet/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Outbound traffic allowed by default", + "FromPort": -1, + "IpProtocol": "-1", + "ToPort": -1 + } + ], + "SecurityGroupIngress": [], + "VpcId": "my-vpc" + } + }, + MyFleetInstanceRole25A84AB8: { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + MyFleetInstanceRoleDefaultPolicy7B0197E7: { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "*", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyFleetInstanceRoleDefaultPolicy7B0197E7", + "Roles": [ + { + "Ref": "MyFleetInstanceRole25A84AB8" + } + ] + } + }, + MyFleetInstanceProfile70A58496: { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "MyFleetInstanceRole25A84AB8" + } + ] + } + }, + MyFleetLaunchConfig5D7F9801: { + Type: "AWS::AutoScaling::LaunchConfiguration", + Properties: { + "IamInstanceProfile": { + "Ref": "MyFleetInstanceProfile70A58496" + }, + "ImageId": "", + "InstanceType": "m4.micro", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "MyFleetInstanceSecurityGroup774E8234", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": "#!/bin/bash\n" + } + }, + DependsOn: [ + "MyFleetInstanceRole25A84AB8", + "MyFleetInstanceRoleDefaultPolicy7B0197E7" + ] + }, + MyFleetASG88E55886: { + Type: "AWS::AutoScaling::AutoScalingGroup", + Properties: { + DesiredCapacity: "1", + LaunchConfigurationName: { + Ref: "MyFleetLaunchConfig5D7F9801" + }, + LoadBalancerNames: [], + MaxSize: "1", + MinSize: "1", + VPCZoneIdentifier: [ + "pri1" + ] + } + } + } + }); + + test.done(); + }, +}; + +function mockVpc(stack: Stack) { + return VpcNetwork.import(stack, 'MyVpc', { + vpcId: new VpcNetworkId('my-vpc'), + availabilityZones: [ 'az1' ], + publicSubnetIds: [ new VpcSubnetId('pub1') ], + privateSubnetIds: [ new VpcSubnetId('pri1') ], + }); +} \ No newline at end of file diff --git a/packages/aws-cdk-ec2/test/test.loadbalancer.ts b/packages/aws-cdk-ec2/test/test.loadbalancer.ts new file mode 100644 index 0000000000000..891cb2f646532 --- /dev/null +++ b/packages/aws-cdk-ec2/test/test.loadbalancer.ts @@ -0,0 +1,32 @@ +import { Stack } from 'aws-cdk'; +import { expect, haveResource } from 'aws-cdk-assert'; +import { Test } from 'nodeunit'; +import { ClassicLoadBalancer, LoadBalancingProtocol, VpcNetwork } from '../lib'; + +export = { + 'test specifying nonstandard port works'(test: Test) { + const stack = new Stack(undefined, undefined, { env: { account: '1234', region: 'test' }}); + stack.setContext('availability-zones:1234:test', ['test-1a', 'test-1b']); + const vpc = new VpcNetwork(stack, 'VCP'); + + const lb = new ClassicLoadBalancer(stack, 'LB', { vpc }); + + lb.addListener({ + externalProtocol: LoadBalancingProtocol.Http, + externalPort: 8080, + internalProtocol: LoadBalancingProtocol.Http, + internalPort: 8080, + }); + + expect(stack).to(haveResource("AWS::ElasticLoadBalancing::LoadBalancer", { + Listeners: [{ + InstancePort: "8080", + InstanceProtocol: "http", + LoadBalancerPort: "8080", + Protocol: "http" + }] + })); + + test.done(); + } +}; \ No newline at end of file diff --git a/packages/aws-cdk-ec2/test/test.network-utils.ts b/packages/aws-cdk-ec2/test/test.network-utils.ts new file mode 100644 index 0000000000000..87eed55ac9108 --- /dev/null +++ b/packages/aws-cdk-ec2/test/test.network-utils.ts @@ -0,0 +1,40 @@ +import { Test } from 'nodeunit'; +import { InvalidCidrRangeError, InvalidSubnetCountError, NetworkUtils } from '../lib/network-util'; + +export = { + + CIDR: { + + "should error when calculating subnets for CIDR range that is too big (10.0.0.0/8)"(test: Test) { + test.throws(() => { + NetworkUtils.splitCIDR('10.0.0.0/8', 2); + }, InvalidCidrRangeError); + test.done(); + }, + + "should error when calculating subnets for CIDR range that is too small (10.0.0.0/32)"(test: Test) { + test.throws(() => { + NetworkUtils.splitCIDR('10.0.0.0/32', 2); + }, InvalidCidrRangeError); + test.done(); + }, + + "should error when trying to split a CIDR range into an unfeasible number of subnets (25 subnets from a /16)"(test: Test) { + test.throws(() => { + NetworkUtils.splitCIDR('10.0.0.0/16', 25); + }, InvalidSubnetCountError); + test.done(); + }, + + "should successfully split a CIDR range into a valid number of subnets (6 subnets from a /16)"(test: Test) { + test.deepEqual(NetworkUtils.splitCIDR('10.0.0.0/16', 6), [ + '10.0.0.0/19', '10.0.32.0/19', + '10.0.64.0/19', '10.0.96.0/19', + '10.0.128.0/19', '10.0.160.0/19' + ]); + test.done(); + } + + } + +}; \ No newline at end of file diff --git a/packages/aws-cdk-ec2/test/test.vpc.ts b/packages/aws-cdk-ec2/test/test.vpc.ts new file mode 100644 index 0000000000000..659bde2289f0c --- /dev/null +++ b/packages/aws-cdk-ec2/test/test.vpc.ts @@ -0,0 +1,117 @@ +import { AvailabilityZoneProvider, Stack } from 'aws-cdk'; +import { countResources, expect, haveResource } from 'aws-cdk-assert'; +import { Test } from 'nodeunit'; +import { DefaultInstanceTenancy, OutboundTrafficMode, VpcNetwork } from '../lib'; + +export = { + + "When creating a VPC with the default CIDR range": { + + "vpc.vpcId returns a token to the VPC ID"(test: Test) { + const stack = getTestStack(); + const vpc = new VpcNetwork(stack, 'TheVPC'); + test.deepEqual(vpc.vpcId.resolve(), {Ref: 'TheVPC92636AB0' } ); + test.done(); + }, + + "it uses the correct network range"(test: Test) { + const stack = getTestStack(); + new VpcNetwork(stack, 'TheVPC'); + expect(stack).to(haveResource('AWS::EC2::VPC', { + CidrBlock: VpcNetwork.DEFAULT_CIDR_RANGE, + EnableDnsHostnames: true, + EnableDnsSupport: true, + InstanceTenancy: DefaultInstanceTenancy.Default, + Tags: [] + })); + test.done(); + }, + + "with all of the properties set, it successfully sets the correct VPC properties"(test: Test) { + const stack = getTestStack(); + const tag = { + key: 'testKey', + value: 'testValue' + }; + new VpcNetwork(stack, 'TheVPC', { + cidr: "192.168.0.0/16", + enableDnsHostnames: false, + enableDnsSupport: false, + defaultInstanceTenancy: DefaultInstanceTenancy.Dedicated, + outboundTraffic: OutboundTrafficMode.None, + tags: [tag] + }); + + expect(stack).to(haveResource('AWS::EC2::VPC', { + CidrBlock: '192.168.0.0/16', + EnableDnsHostnames: false, + EnableDnsSupport: false, + InstanceTenancy: DefaultInstanceTenancy.Dedicated, + Tags: [{ Key: tag.key, Value: tag.value }] + })); + test.done(); + }, + + "contains the correct number of subnets"(test: Test) { + const stack = getTestStack(); + const vpc = new VpcNetwork(stack, 'TheVPC'); + const zones = new AvailabilityZoneProvider(stack).availabilityZones.length; + test.equal(vpc.publicSubnets.length, zones); + test.equal(vpc.privateSubnets.length, zones); + test.deepEqual(vpc.vpcId.resolve(), { Ref: 'TheVPC92636AB0' }); + test.done(); + }, + + "with outbound traffic mode None, the VPC should not contain an IGW or NAT Gateways"(test: Test) { + const stack = getTestStack(); + new VpcNetwork(stack, 'TheVPC', { outboundTraffic: OutboundTrafficMode.None }); + expect(stack).notTo(haveResource("AWS::EC2::InternetGateway")); + expect(stack).notTo(haveResource("AWS::EC2::NatGateway")); + test.done(); + }, + + "with outbound traffic mode FromPublicSubnetsOnly, the VPC should have an IGW but no NAT Gateways"(test: Test) { + const stack = getTestStack(); + new VpcNetwork(stack, 'TheVPC', { outboundTraffic: OutboundTrafficMode.FromPublicSubnetsOnly }); + expect(stack).to(countResources('AWS::EC2::InternetGateway', 1)); + expect(stack).notTo(haveResource("AWS::EC2::NatGateway")); + test.done(); + }, + + "with outbound traffic mode FromPublicAndPrivateSubnets, the VPC should have an IGW, and a NAT Gateway per AZ"(test: Test) { + const stack = getTestStack(); + const zones = new AvailabilityZoneProvider(stack).availabilityZones.length; + new VpcNetwork(stack, 'TheVPC', { outboundTraffic: OutboundTrafficMode.FromPublicAndPrivateSubnets }); + expect(stack).to(countResources("AWS::EC2::InternetGateway", 1)); + expect(stack).to(countResources("AWS::EC2::NatGateway", zones)); + test.done(); + }, + + "with enableDnsHostnames enabled but enableDnsSupport disabled, should throw an Error"(test: Test) { + const stack = getTestStack(); + test.throws(() => new VpcNetwork(stack, 'TheVPC', { + enableDnsHostnames: true, + enableDnsSupport: false + })); + test.done(); + } + + }, + + "When creating a VPC with a custom CIDR range": { + "vpc.vpcCidrBlock is the correct network range"(test: Test) { + const stack = getTestStack(); + new VpcNetwork(stack, 'TheVPC', { cidr: '192.168.0.0/16' }); + expect(stack).to(haveResource("AWS::EC2::VPC", { + CidrBlock: '192.168.0.0/16' + })); + test.done(); + } + + } + +}; + +function getTestStack(): Stack { + return new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); +} \ No newline at end of file