From 939452c6d6baf13dadef86e1f7194aa320392096 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 23 Aug 2018 14:31:20 -0700 Subject: [PATCH 001/140] Ignore swp files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7b89f50668180..5a03c2b774932 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ pack coverage .nyc_output .LAST_BUILD +*.swp From 7302a2f4250659f5e12c6ea6c9a2264e6f92b885 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 23 Aug 2018 14:30:54 -0700 Subject: [PATCH 002/140] Add ECS lib and example --- .../hello-cdk-ecs/cdk.json | 26 ++++++++ .../hello-cdk-ecs/index.ts | 15 +++++ examples/cdk-examples-typescript/package.json | 1 + packages/@aws-cdk/aws-ecs/lib/cluster.ts | 59 +++++++++++++++++++ packages/@aws-cdk/aws-ecs/lib/index.ts | 1 + packages/@aws-cdk/aws-ecs/package.json | 4 +- 6 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json create mode 100644 examples/cdk-examples-typescript/hello-cdk-ecs/index.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/cluster.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json new file mode 100644 index 0000000000000..3d3c2f9878030 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json @@ -0,0 +1,26 @@ +{ + "app": "node index", + "context": { + "availability-zones:585695036304:us-east-1": [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f" + ], + "ssm:585695036304:us-east-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-14c5486b", + "availability-zones:585695036304:eu-west-2": [ + "eu-west-2a", + "eu-west-2b", + "eu-west-2c" + ], + "ssm:585695036304:eu-west-2:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-a36f8dc4", + "availability-zones:585695036304:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" + ], + "ssm:585695036304:eu-west-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-ca0135b3" + } +} \ No newline at end of file diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts new file mode 100644 index 0000000000000..948b831ecf3b5 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -0,0 +1,15 @@ +import ecs = require('@aws-cdk/aws-ecs'); +import cdk = require('@aws-cdk/cdk'); + +class HelloECS extends cdk.Stack { + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + new ecs.Cluster(this, 'MyCluster'); + } +} + +const app = new cdk.App(process.argv); + +new HelloECS(app, 'Hello'); + +process.stdout.write(app.run()); diff --git a/examples/cdk-examples-typescript/package.json b/examples/cdk-examples-typescript/package.json index 3a2ef91dcbf36..fff2dee2aed30 100644 --- a/examples/cdk-examples-typescript/package.json +++ b/examples/cdk-examples-typescript/package.json @@ -28,6 +28,7 @@ "@aws-cdk/aws-cognito": "^0.8.2", "@aws-cdk/aws-dynamodb": "^0.8.2", "@aws-cdk/aws-ec2": "^0.8.2", + "@aws-cdk/aws-ecs": "^0.8.2", "@aws-cdk/aws-iam": "^0.8.2", "@aws-cdk/aws-lambda": "^0.8.2", "@aws-cdk/aws-neptune": "^0.8.2", diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts new file mode 100644 index 0000000000000..d70b930073fa1 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -0,0 +1,59 @@ +import autoscaling = require('@aws-cdk/aws-autoscaling'); +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { cloudformation, ClusterArn} from './ecs.generated'; + +export interface ClusterProps { + /** + * A name for the cluster. + * @default CloudFormation-generated name + */ + clusterName?: string; + + vpc: ec2.VpcNetworkRef; + + asg: autoscaling.AutoScalingGroup; +} + +export class ClusterName extends cdk.Token { +} + +export class Cluster extends cdk.Construct { + + public readonly clusterArn: ClusterArn; + + public readonly clusterName: ClusterName; + + constructor(parent: cdk.Construct, name: string, props: ClusterProps = {}) { + super(parent, name); + + const cluster = new cloudformation.ClusterResource(this, "Resource", {clusterName: props.clusterName}); + + this.clusterArn = cluster.clusterArn; + + this.clusterName = new ClusterName(cluster.ref); + + const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { + family: "Service", + memory: "512", + cpu: "256", + containerDefinitions: [{ + name: "web", + image: "amazon/amazon-ecs-sample", + cpu: 10, + memory: 1024, + essential: true + }] + }); + + new cloudformation.ServiceResource(this, "Service", { + cluster: this.clusterName, + taskDefinition: taskDef.ref, + desiredCount: 1, + deploymentConfiguration: { + maximumPercent: 200, + minimumHealthyPercent: 75 + } + }); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 8cccdf3fb43d7..fe07251d7062c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,2 +1,3 @@ // AWS::ECS CloudFormation Resources: export * from './ecs.generated'; +export * from './cluster'; diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 861cf4d400b82..28916a84773f3 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -52,7 +52,9 @@ "pkglint": "^0.8.2" }, "dependencies": { - "@aws-cdk/cdk": "^0.8.2" + "@aws-cdk/cdk": "^0.8.2", + "@aws-cdk/aws-autoscaling": "^0.8.2", + "@aws-cdk/aws-ec2": "^0.8.2" }, "homepage": "https://github.com/awslabs/aws-cdk" } From 4c7127a3affcf14912dbac064cf837164ff021ae Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 24 Aug 2018 01:30:34 -0700 Subject: [PATCH 003/140] Add service and task def constructs --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 27 ++++----------- packages/@aws-cdk/aws-ecs/lib/index.ts | 5 ++- packages/@aws-cdk/aws-ecs/lib/service.ts | 26 ++++++++++++++ .../@aws-cdk/aws-ecs/lib/task-definition.ts | 34 +++++++++++++++++++ 4 files changed, 71 insertions(+), 21 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/service.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/task-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index d70b930073fa1..a716a9385fdbf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -2,6 +2,8 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation, ClusterArn} from './ecs.generated'; +import { Service } from './service'; +import { TaskDefinition } from './task-definition'; export interface ClusterProps { /** @@ -10,9 +12,9 @@ export interface ClusterProps { */ clusterName?: string; - vpc: ec2.VpcNetworkRef; + vpc?: ec2.VpcNetworkRef; - asg: autoscaling.AutoScalingGroup; + asg?: autoscaling.AutoScalingGroup; } export class ClusterName extends cdk.Token { @@ -33,27 +35,12 @@ export class Cluster extends cdk.Construct { this.clusterName = new ClusterName(cluster.ref); - const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { - family: "Service", - memory: "512", - cpu: "256", - containerDefinitions: [{ - name: "web", - image: "amazon/amazon-ecs-sample", - cpu: 10, - memory: 1024, - essential: true - }] - }); + const taskDef = new TaskDefinition(this, "MyTD"); - new cloudformation.ServiceResource(this, "Service", { + new Service(this, "Service", { cluster: this.clusterName, - taskDefinition: taskDef.ref, + taskDefinition: taskDef.taskDefinitionArn, desiredCount: 1, - deploymentConfiguration: { - maximumPercent: 200, - minimumHealthyPercent: 75 - } }); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index fe07251d7062c..53babeb50b7a4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,3 +1,6 @@ +export * from './cluster'; +export * from './service'; +export * from './task-definition'; + // AWS::ECS CloudFormation Resources: export * from './ecs.generated'; -export * from './cluster'; diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts new file mode 100644 index 0000000000000..5ae7537885b35 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -0,0 +1,26 @@ +import cdk = require('@aws-cdk/cdk'); +import { ClusterName } from './cluster'; +import { TaskDefinitionArn } from './task-definition'; +import { cloudformation } from './ecs.generated'; + +export interface ServiceProps { + cluster: ClusterName; + taskDefinition: TaskDefinitionArn; + desiredCount: number; +} + +export class Service extends cdk.Construct { + constructor(parent: cdk.Construct, name: string, props: ServiceProps) { + super(parent, name); + + new cloudformation.ServiceResource(this, "Service", { + cluster: props.cluster, + taskDefinition: props.taskDefinition, + desiredCount: props.desiredCount, + deploymentConfiguration: { + maximumPercent: 200, + minimumHealthyPercent: 75 + } + }); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts new file mode 100644 index 0000000000000..328c33fe0690e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -0,0 +1,34 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from './ecs.generated'; + +export interface TaskDefinitionProps { + cpu?: string; + memory?: string; + // specify cpu, memory, image url for containers +} + +export class TaskDefinitionArn extends cdk.Token { +} + +export class TaskDefinition extends cdk.Construct { + public readonly taskDefinitionArn: TaskDefinitionArn; + + constructor(parent: cdk.Construct, name: string, _props: TaskDefinitionProps = {}) { + super(parent, name); + + const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { + family: "ecs-demo", + memory: "512", + cpu: "256", + containerDefinitions: [{ + name: "web", + image: "amazon/amazon-ecs-sample", + cpu: 10, + memory: 128, + essential: true + }] + }); + + this.taskDefinitionArn = taskDef.ref; + } +} From 98749ec3f0694f294a32beae615128a960419d9f Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 24 Aug 2018 02:15:57 -0700 Subject: [PATCH 004/140] WIP adding VPC/ASG --- .../hello-cdk-ecs/cdk.json | 9 +++++++-- .../hello-cdk-ecs/index.ts | 6 +++--- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json index 3d3c2f9878030..a49176a091559 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json @@ -21,6 +21,11 @@ "eu-west-1b", "eu-west-1c" ], - "ssm:585695036304:eu-west-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-ca0135b3" + "ssm:585695036304:eu-west-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-ca0135b3", + "availability-zones:794715269151:us-west-2": [ + "us-west-2a", + "us-west-2b", + "us-west-2c" + ] } -} \ No newline at end of file +} diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 948b831ecf3b5..9fce96e972253 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -1,15 +1,15 @@ import ecs = require('@aws-cdk/aws-ecs'); import cdk = require('@aws-cdk/cdk'); -class HelloECS extends cdk.Stack { +class BonjourECS extends cdk.Stack { constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { super(parent, name, props); - new ecs.Cluster(this, 'MyCluster'); + new ecs.Cluster(this, 'DemoCluster'); } } const app = new cdk.App(process.argv); -new HelloECS(app, 'Hello'); +new BonjourECS(app, 'Goede Morgen'); process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index a716a9385fdbf..b3ecad800d452 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -26,6 +26,8 @@ export class Cluster extends cdk.Construct { public readonly clusterName: ClusterName; + public readonly fleet: autoscaling.AutoScalingGroup; + constructor(parent: cdk.Construct, name: string, props: ClusterProps = {}) { super(parent, name); @@ -42,5 +44,18 @@ export class Cluster extends cdk.Construct { taskDefinition: taskDef.taskDefinitionArn, desiredCount: 1, }); + + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { + maxAZs: 2 + }); + + const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { + vpc, + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), + machineImage: new ec2.GenericLinuxImage({'us-west-2':'ami-00d4f478'}) + }); + + this.fleet = fleet; + } } From 6d0942fcd1916867732300a609589301b318c9e7 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 24 Aug 2018 14:36:51 +0200 Subject: [PATCH 005/140] Refactor ECS demo - Fix linting errors - VPC is now an argument and not created inside the cluster - Link the AutoScalingGroup up to the cluster via userData - Give the ASG instance role the required IAM permissions - Extend ECS-optimized AMI table with all regions - Add an example configuration option to deny containers access to EC2 instance metadata service. --- .../hello-cdk-ecs/cdk.json | 10 ++ .../hello-cdk-ecs/index.ts | 16 +++- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 95 ++++++++++++++++--- packages/@aws-cdk/aws-ecs/lib/service.ts | 2 +- 4 files changed, 109 insertions(+), 14 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json index a49176a091559..841c948f0b918 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json @@ -26,6 +26,16 @@ "us-west-2a", "us-west-2b", "us-west-2c" + ], + "availability-zones:993655754359:us-west-2": [ + "us-west-2a", + "us-west-2b", + "us-west-2c" + ], + "availability-zones:993655754359:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" ] } } diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 9fce96e972253..186fdcb54edb9 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -1,15 +1,27 @@ +import ec2 = require('@aws-cdk/aws-ec2'); import ecs = require('@aws-cdk/aws-ecs'); import cdk = require('@aws-cdk/cdk'); class BonjourECS extends cdk.Stack { constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { super(parent, name, props); - new ecs.Cluster(this, 'DemoCluster'); + + // For better iteration speed, it might make sense to put this VPC into + // a separate stack and import it here. We then have two stacks to + // deploy, but VPC creation is slow so we'll only have to do that once + // and can iterate quickly on consuming stacks. Not doing that for now. + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { + maxAZs: 2 + }); + + new ecs.Cluster(this, 'DemoCluster', { + vpc + }); } } const app = new cdk.App(process.argv); -new BonjourECS(app, 'Goede Morgen'); +new BonjourECS(app, 'GoedeMorgen'); process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index b3ecad800d452..2bf7228a6e07b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -8,16 +8,47 @@ import { TaskDefinition } from './task-definition'; export interface ClusterProps { /** * A name for the cluster. + * * @default CloudFormation-generated name */ clusterName?: string; - vpc?: ec2.VpcNetworkRef; + /** + * The VPC where your ECS instances will be running + */ + vpc: ec2.VpcNetworkRef; - asg?: autoscaling.AutoScalingGroup; + /** + * Whether or not the containers can access the instance role + * + * @default false + */ + containersAccessInstanceRole?: boolean; } -export class ClusterName extends cdk.Token { +// This works for now but how will we keep this list up to date? +export const ECS_OPTIMIZED_AMI = new ec2.GenericLinuxImage({ + 'us-east-2': 'ami-028a9de0a7e353ed9', + 'us-east-1': 'ami-00129b193dc81bc31', + 'us-west-2': 'ami-00d4f478', + 'us-west-1': 'ami-0d438d09af26c9583', + 'eu-west-2': 'ami-a44db8c3', + 'eu-west-3': 'ami-07da674f0655ef4e1', + 'eu-west-1': 'ami-0af844a965e5738db', + 'eu-central-1': 'ami-0291ba887ba0d515f', + 'ap-northeast-2': 'ami-047d2a61f94f862dc', + 'ap-northeast-1': 'ami-0041c416aa23033a2', + 'ap-southeast-2': 'ami-0092e55c70015d8c3', + 'ap-southeast-1': 'ami-091bf462afdb02c60', + 'ca-central-1': 'ami-192fa27d', + 'ap-south-1': 'ami-0c179ca015d301829', + 'sa-east-1': 'ami-0018ff8ee48970ac3', + 'us-gov-west-1': 'ami-c6079ba7', +}); + +// Needs to inherit from CloudFormationToken to make the string substitution +// downstairs work. This is temporary, will go away in the near future. +export class ClusterName extends cdk.CloudFormationToken { } export class Cluster extends cdk.Construct { @@ -26,9 +57,9 @@ export class Cluster extends cdk.Construct { public readonly clusterName: ClusterName; - public readonly fleet: autoscaling.AutoScalingGroup; + public readonly fleet: autoscaling.AutoScalingGroup; - constructor(parent: cdk.Construct, name: string, props: ClusterProps = {}) { + constructor(parent: cdk.Construct, name: string, props: ClusterProps) { super(parent, name); const cluster = new cloudformation.ClusterResource(this, "Resource", {clusterName: props.clusterName}); @@ -45,16 +76,58 @@ export class Cluster extends cdk.Construct { desiredCount: 1, }); - const vpc = new ec2.VpcNetwork(this, 'MyVpc', { - maxAZs: 2 - }); - const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { - vpc, + vpc: props.vpc, instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), - machineImage: new ec2.GenericLinuxImage({'us-west-2':'ami-00d4f478'}) + machineImage: ECS_OPTIMIZED_AMI, + updateType: autoscaling.UpdateType.ReplacingUpdate }); + // Tie instances to cluster + fleet.addUserData(`echo ECS_CLUSTER=${this.clusterName} >> /etc/ecs/ecs.config`); + + if (!props.containersAccessInstanceRole) { + // Deny containers access to instance metadata service + // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html + fleet.addUserData('sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP'); + fleet.addUserData('sudo service iptables save'); + } + + // Note: if the fleet doesn't launch or doesn't register itself with + // ECS, *Cluster* stabilization will fail after timing our for an hour + // or so, because the *Service* doesn't have any running instances. + // During this time, you CANNOT DO ANYTHING ELSE WITH YOUR STACK. + // + // Apart from the weird relationship here between Cluster and Service + // (why is Cluster failing and not Service?), the experience is... + // + // NOT GREAT. + // + // Also, there's sort of a bidirectional dependency between Cluster and ASG: + // + // - ASG depends on Cluster to get the ClusterName (which needs to go into + // UserData). + // - Cluster depends on ASG to boot up, so the service is launched, so the + // Cluster can stabilize. + + // ECS instances must be able to do these things + // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html + fleet.addToRolePolicy(new cdk.PolicyStatement().addActions( + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "logs:CreateLogStream", + "logs:PutLogEvents" + ).addAllResources()); // Conceivably we might do better than all resources and add targeted ARNs + this.fleet = fleet; } diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts index 5ae7537885b35..3e03940620b12 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -1,7 +1,7 @@ import cdk = require('@aws-cdk/cdk'); import { ClusterName } from './cluster'; -import { TaskDefinitionArn } from './task-definition'; import { cloudformation } from './ecs.generated'; +import { TaskDefinitionArn } from './task-definition'; export interface ServiceProps { cluster: ClusterName; From bddfe561b526a2b0c8e427630e23fbfabda50ebb Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 24 Aug 2018 08:17:03 -0700 Subject: [PATCH 006/140] Add more props on Service --- packages/@aws-cdk/aws-ecs/lib/service.ts | 72 ++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts index 3e03940620b12..a6672544b89c5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -4,9 +4,68 @@ import { cloudformation } from './ecs.generated'; import { TaskDefinitionArn } from './task-definition'; export interface ServiceProps { - cluster: ClusterName; + /** + * Cluster where service will be deployed + */ + cluster: ClusterName; // should be required? do we assume 'default' exists? + + /** + * Task Definition used for running tasks in the service + */ taskDefinition: TaskDefinitionArn; - desiredCount: number; + + /** + * Number of desired copies of running tasks + * + * @default 1 + */ + desiredCount?: number; + + /** + * A name for the service. + * + * @default CloudFormation-generated name + */ + serviceName?: string; + + /** + * Whether the service is hosted in EC2 or Fargate + * + * @default EC2 + */ + launchType?: string; // maybe unnecessary if we have different ECS vs FG service + + /** + * The maximum number of tasks, specified as a percentage of the Amazon ECS service's DesiredCount value, that can run in a service during a deployment. + * + * @default 200 + */ + maximumPercent?: number; + + /** + * The minimum number of tasks, specified as a percentage of + * the Amazon ECS service's DesiredCount value, that must + * continue to run and remain healthy during a deployment. + * + * @default 50 + */ + minimumHealthyPercent?: number; + + /** + * The name or ARN of an AWS Identity and Access Management (IAM) role that allows your Amazon ECS container agent to make calls to your load balancer. + */ + role?: string; + + ///////// TBD /////////////////////////////// + // healthCheckGracePeriodSeconds?: number; // only needed with load balancers + // loadBalancers?: LoadBalancer[]; + // placementConstraints?: PlacementConstraint[]; + // placementStrategies?: PlacementStrategy[]; + // networkConfiguration?: NetworkConfiguration; + // serviceRegistries?: ServiceRegistry[]; + // + // platformVersion?: string; // FARGATE ONLY. default is LATEST. Other options: 1.2.0, 1.1.0, 1.0.0 + //////////////////////////////////////////// } export class Service extends cdk.Construct { @@ -17,10 +76,13 @@ export class Service extends cdk.Construct { cluster: props.cluster, taskDefinition: props.taskDefinition, desiredCount: props.desiredCount, + serviceName: props.serviceName, + launchType: props.launchType, deploymentConfiguration: { - maximumPercent: 200, - minimumHealthyPercent: 75 - } + maximumPercent: props.maximumPercent, + minimumHealthyPercent: props.minimumHealthyPercent + }, + role: props.role, }); } } From 3574611ec4a578ca9ec75dd7efa5d6eca1500a5c Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 6 Sep 2018 14:48:41 -0700 Subject: [PATCH 007/140] Add TODO for AMI IDs --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 2bf7228a6e07b..185a02d7e41e7 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -26,7 +26,7 @@ export interface ClusterProps { containersAccessInstanceRole?: boolean; } -// This works for now but how will we keep this list up to date? +// TODO: replace this with call to SSM, stored as "/aws/service/ecs/optimized-ami/amazon-linux/recommended" export const ECS_OPTIMIZED_AMI = new ec2.GenericLinuxImage({ 'us-east-2': 'ami-028a9de0a7e353ed9', 'us-east-1': 'ami-00129b193dc81bc31', From a63c377e4fb9c3c90d61467d239732a0b4288fa9 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 6 Sep 2018 17:43:10 -0700 Subject: [PATCH 008/140] Weird hack? Was getting error: lib/index.ts(7,1): error TS2308: Module './cluster' has already exported a member named 'ClusterName'. Consider explicitly re-exporting to resolve the ambiguity. lib/index.ts(7,1): error TS2308: Module './task-definition' has already exported a member named 'TaskDefinitionArn'. Consider explicitly re-exporting to resolve the ambiguity. This seemed to make npm run build happy --- packages/@aws-cdk/aws-ecs/lib/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 53babeb50b7a4..4a2b652775532 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -3,4 +3,4 @@ export * from './service'; export * from './task-definition'; // AWS::ECS CloudFormation Resources: -export * from './ecs.generated'; +export {ClusterName, TaskDefinitionArn} from './ecs.generated'; From b00e0dd52ad5682fb90d180fe18fc554daa88833 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 6 Sep 2018 18:59:36 -0700 Subject: [PATCH 009/140] Update example to instantiate service separately --- .../cdk-examples-typescript/hello-cdk-ecs/index.ts | 10 +++++++++- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 10 ---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 186fdcb54edb9..b31a9a8ddf16a 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -14,9 +14,17 @@ class BonjourECS extends cdk.Stack { maxAZs: 2 }); - new ecs.Cluster(this, 'DemoCluster', { + const cluster = new ecs.Cluster(this, 'DemoCluster', { vpc }); + + const taskDef = new ecs.TaskDefinition(this, "MyTD"); + + new ecs.Service(this, "Service", { + cluster: cluster.clusterName, + taskDefinition: taskDef.taskDefinitionArn, + desiredCount: 1, + }); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 185a02d7e41e7..9b5cd69431375 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -2,8 +2,6 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation, ClusterArn} from './ecs.generated'; -import { Service } from './service'; -import { TaskDefinition } from './task-definition'; export interface ClusterProps { /** @@ -68,14 +66,6 @@ export class Cluster extends cdk.Construct { this.clusterName = new ClusterName(cluster.ref); - const taskDef = new TaskDefinition(this, "MyTD"); - - new Service(this, "Service", { - cluster: this.clusterName, - taskDefinition: taskDef.taskDefinitionArn, - desiredCount: 1, - }); - const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { vpc: props.vpc, instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), From 80ba2e467a490a29efe800a1ea85b8582b0c8c65 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 11 Sep 2018 23:04:05 -0700 Subject: [PATCH 010/140] WIP Add Container Definitions --- .../hello-cdk-ecs/index.ts | 26 +++- .../@aws-cdk/aws-ecs/lib/task-definition.ts | 121 ++++++++++++++++-- 2 files changed, 135 insertions(+), 12 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index b31a9a8ddf16a..72651448ed449 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -18,7 +18,31 @@ class BonjourECS extends cdk.Stack { vpc }); - const taskDef = new ecs.TaskDefinition(this, "MyTD"); + // name, image, cpu, memory, port (with default) + // + // Include in constructs: + // - networking - include SD, ALB + // - logging - cloudwatch logs integration? talk to nathan about 3rd + // party integrations - aggregated logging across the service + // (instead of per task). Probably prometheus or elk? + // - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, + // zipkin. + // - so x-ray is a container that is hooked up to sidecars that come + // with the application container itself + // - autoscaling - application autoscaling (Fargate focused?) + + const taskDef = new ecs.TaskDefinition(this, "MyTD", { + family: "ecs-task-definition", + containerDefinitions: [ + { + name: "web", + image: "amazon/amazon-ecs-sample", + cpu: 1024, + memory: 512, + essential: true + } + ] + }); new ecs.Service(this, "Service", { cluster: cluster.clusterName, diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 328c33fe0690e..1492e7629996f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -2,9 +2,61 @@ import cdk = require('@aws-cdk/cdk'); import { cloudformation } from './ecs.generated'; export interface TaskDefinitionProps { + /** + * The number of cpu units used by the task. If using the EC2 launch type, + * this field is optional. Supported values are between 128 CPU units + * (0.125 vCPUs) and 10240 CPU units (10 vCPUs). If you are using the + * Fargate launch type, this field is required and you must use one of the + * following values, which determines your range of valid values for the + * memory parameter: + * 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB + * 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB + * 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB + * 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments + * 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments + * + * @default 256 + */ cpu?: string; + + /** + * The amount (in MiB) of memory used by the task. If using the EC2 launch + * type, this field is optional and any value can be used. If you are using + * the Fargate launch type, this field is required and you must use one of + * the following values, which determines your range of valid values for + * the cpu parameter: + * + * 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU) + * + * 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU) + * + * 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU) + * + * Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU) + * + * Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU) + * + * @default 512 + */ memory?: string; - // specify cpu, memory, image url for containers + + /** + * Namespace for task definition versions + * + * @default CloudFormation-generated name + */ + family?: string; + + // taskRoleArn?: string; // goes with container defs? + + networkMode?: string; + + containerDefinitions: ContainerDefinition[]; + // + // executionRoleArn:? string; + // volumes?: VolumeDefinition[]; + // placementConstraints?: PlacementConstraint[]; + // requiresCompatibilities?: string[]; // FARGATE or EC2 -- set on ECS TD vs FG TD } export class TaskDefinitionArn extends cdk.Token { @@ -12,23 +64,70 @@ export class TaskDefinitionArn extends cdk.Token { export class TaskDefinition extends cdk.Construct { public readonly taskDefinitionArn: TaskDefinitionArn; + // public readonly containerDefinitions: ContainerDefinition[]; + private readonly containerDefinitions: cloudformation.TaskDefinitionResource.ContainerDefinitionProperty[] = []; - constructor(parent: cdk.Construct, name: string, _props: TaskDefinitionProps = {}) { + constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { super(parent, name); const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { family: "ecs-demo", - memory: "512", - cpu: "256", - containerDefinitions: [{ - name: "web", - image: "amazon/amazon-ecs-sample", - cpu: 10, - memory: 128, - essential: true - }] + memory: props.memory, + cpu: props.cpu, + containerDefinitions: new cdk.Token(() => this.containerDefinitions), // ???? }); + props.containerDefinitions.forEach(c => this.addContainer(c)); + this.taskDefinitionArn = taskDef.ref; } + + private addContainer(definition:ContainerDefinition) { + const cd = this.renderContainerDefininition(definition); + this.containerDefinitions.push(cd); + } + + // Populates task definition with container definition + private renderContainerDefininition(definition: ContainerDefinition): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { + return { + name: definition.name, + image: definition.image, + cpu: definition.cpu, + memory: definition.memory, + essential: definition.essential, + command: definition.command + }; + } +} + +export interface ContainerDefinition{ + name: string; + image: string; + + command?: string[]; + cpu?: number; + disableNetworking?: boolean; + dnsSearchDomains?: string[]; + dnsServers?: string[]; + dockerLabels?: string[]; + dockerSecurityOptions?: string[]; + entryPoint?: string[]; + essential?: boolean; + hostname?: string; + links?: string[]; + memory?: number; + memoryReservation?: number; + privileged?: boolean; + readonlyRootFilesystem?: boolean; + user?: string; + workingDirectory?: string } + // environment?: list of key-value; + // extraHosts?: hostEntry[]; + // healthCheck?: healthCheck; + // linuxParameters: linuxParam[]; + // logConfiguration: logConfig[]; + // mountPoints?: mountPoint[]; + // portMappings?: portMapping[]; + // ulimits?: ulimit[]; + // volumesFrom?: volumeFrom[]; From 0980f520f4d30be04379dff199e463e0c8928975 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 14 Sep 2018 14:48:53 -0700 Subject: [PATCH 011/140] WIP Add other Task Def properties --- .../hello-cdk-ecs/index.ts | 3 + packages/@aws-cdk/aws-ecs/lib/cluster.ts | 1 + packages/@aws-cdk/aws-ecs/lib/service.ts | 8 +- .../@aws-cdk/aws-ecs/lib/task-definition.ts | 187 +++++++++++++++--- 4 files changed, 167 insertions(+), 32 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 72651448ed449..08f3ba4220012 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -33,6 +33,9 @@ class BonjourECS extends cdk.Stack { const taskDef = new ecs.TaskDefinition(this, "MyTD", { family: "ecs-task-definition", + placementConstraints: [{ + type: "distinctInstance" + }], containerDefinitions: [ { name: "web", diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 9b5cd69431375..bae9f5823afcd 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -25,6 +25,7 @@ export interface ClusterProps { } // TODO: replace this with call to SSM, stored as "/aws/service/ecs/optimized-ami/amazon-linux/recommended" +// https://raw.githubusercontent.com/awslabs/aws-cloudformation-templates/master/aws/services/ECS/EC2LaunchType/clusters/public-vpc.yml export const ECS_OPTIMIZED_AMI = new ec2.GenericLinuxImage({ 'us-east-2': 'ami-028a9de0a7e353ed9', 'us-east-1': 'ami-00129b193dc81bc31', diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts index a6672544b89c5..9aaffd7ff3a69 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -36,7 +36,9 @@ export interface ServiceProps { launchType?: string; // maybe unnecessary if we have different ECS vs FG service /** - * The maximum number of tasks, specified as a percentage of the Amazon ECS service's DesiredCount value, that can run in a service during a deployment. + * The maximum number of tasks, specified as a percentage of the Amazon ECS + * service's DesiredCount value, that can run in a service during a + * deployment. * * @default 200 */ @@ -52,7 +54,9 @@ export interface ServiceProps { minimumHealthyPercent?: number; /** - * The name or ARN of an AWS Identity and Access Management (IAM) role that allows your Amazon ECS container agent to make calls to your load balancer. + * The name or ARN of an AWS Identity and Access Management (IAM) role that + * allows your Amazon ECS container agent to make calls to your load + * balancer. */ role?: string; diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 1492e7629996f..4b2f02d235ebf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -2,6 +2,9 @@ import cdk = require('@aws-cdk/cdk'); import { cloudformation } from './ecs.generated'; export interface TaskDefinitionProps { + + containerDefinitions: ContainerDefinition[]; + /** * The number of cpu units used by the task. If using the EC2 launch type, * this field is optional. Supported values are between 128 CPU units @@ -19,6 +22,22 @@ export interface TaskDefinitionProps { */ cpu?: string; + /** + * The Amazon Resource Name (ARN) of the task execution role that + * containers in this task can assume. All containers in this task are + * granted the permissions that are specified in this role. + * + * Needed in Fargate to communicate with Cloudwatch Logs and ECR. + */ + executionRoleArn?: string; + + /** + * Namespace for task definition versions + * + * @default CloudFormation-generated name + */ + family?: string; + /** * The amount (in MiB) of memory used by the task. If using the EC2 launch * type, this field is optional and any value can be used. If you are using @@ -41,22 +60,40 @@ export interface TaskDefinitionProps { memory?: string; /** - * Namespace for task definition versions + * The Docker networking mode to use for the containers in the task, such as none, bridge, or host. + * For Fargate or to use task networking, "awsvpc" mode is required. * - * @default CloudFormation-generated name + * @default bridge */ - family?: string; - - // taskRoleArn?: string; // goes with container defs? - networkMode?: string; - containerDefinitions: ContainerDefinition[]; - // - // executionRoleArn:? string; - // volumes?: VolumeDefinition[]; - // placementConstraints?: PlacementConstraint[]; + /** + * An array of placement constraint objects to use for the task. You can + * specify a maximum of 10 constraints per task (this limit includes + * constraints in the task definition and those specified at run time). + * + * Not supported in Fargate. + */ + placementConstraints?: PlacementConstraint[]; + + /** + * Valid values include EC2 and FARGATE. + * + * @default EC2 + */ // requiresCompatibilities?: string[]; // FARGATE or EC2 -- set on ECS TD vs FG TD + + /** + * The Amazon Resource Name (ARN) of an AWS Identity and Access Management + * (IAM) role that grants containers in the task permission to call AWS + * APIs on your behalf + */ + taskRoleArn?: string; + + /** + * See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide//task_definition_parameters.html#volumes + */ + volumes?: Volume[]; } export class TaskDefinitionArn extends cdk.Token { @@ -64,43 +101,109 @@ export class TaskDefinitionArn extends cdk.Token { export class TaskDefinition extends cdk.Construct { public readonly taskDefinitionArn: TaskDefinitionArn; - // public readonly containerDefinitions: ContainerDefinition[]; private readonly containerDefinitions: cloudformation.TaskDefinitionResource.ContainerDefinitionProperty[] = []; + private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; + private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { super(parent, name); + props.containerDefinitions.forEach(cd => this.addContainer(cd)); + + if (props.placementConstraints) { + props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); + } + + if (props.volumes) { + props.volumes.forEach(v => this.addVolume(v)); + } + const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { - family: "ecs-demo", - memory: props.memory, + containerDefinitions: new cdk.Token(() => this.containerDefinitions), cpu: props.cpu, - containerDefinitions: new cdk.Token(() => this.containerDefinitions), // ???? + executionRoleArn: props.executionRoleArn, + family: props.family, + memory: props.memory, + networkMode: props.networkMode, + placementConstraints: new cdk.Token(() => this.placementConstraints), + taskRoleArn: props.taskRoleArn }); - props.containerDefinitions.forEach(c => this.addContainer(c)); - this.taskDefinitionArn = taskDef.ref; } - private addContainer(definition:ContainerDefinition) { + private addContainer(definition: ContainerDefinition) { const cd = this.renderContainerDefininition(definition); this.containerDefinitions.push(cd); } + private addPlacementConstraint(constraint: PlacementConstraint) { + const pc = this.renderPlacementConstraint(constraint); + this.placementConstraints.push(pc); + } + + private addVolume(volume: Volume) { + // const v = this.renderVolume(volume); + this.volumes.push(volume); + } + // Populates task definition with container definition private renderContainerDefininition(definition: ContainerDefinition): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { + // const logConfigs = this.renderLogConfiguration(definition.logConfiguration); // what to do if undefined? + return { name: definition.name, image: definition.image, + command: definition.command, cpu: definition.cpu, - memory: definition.memory, + disableNetworking: definition.disableNetworking, + dnsSearchDomains: definition.dnsSearchDomains, + dnsServers: definition.dnsServers, + // dockerLabels: definition.dockerLabels, + dockerSecurityOptions: definition.dockerSecurityOptions, + entryPoint: definition.entryPoint, essential: definition.essential, - command: definition.command + hostname: definition.hostname, + links: definition.links, + // logConfiguration: logConfigs, // only set if passed in? + memory: definition.memory, + memoryReservation: definition.memoryReservation, + privileged: definition.privileged, + readonlyRootFilesystem: definition.readonlyRootFilesystem, + user: definition.user, + workingDirectory: definition.workingDirectory }; } + + private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { + return { + type: pc.type, + expression: pc.expression + }; + } + + // private renderLogConfiguration(lc: LogConfiguration[]): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty.LogConfiguration[] { + // return { + // logDriver: lc.logDriver, + // options: lc.options + // }; + // } + + // private renderVolume(volume: Volume): cloudformation.TaskDefinitionResource.VolumeProperty { + // return { + // host: this.renderHost(volume.host), + // name: volume.name + // }; + // } + + // private renderHost(host: Host): cloudformation.TaskDefinitionResource.VolumeProperty.Host { + // return { + // sourcePath: host.sourcePath + // } + // } } -export interface ContainerDefinition{ +export interface ContainerDefinition { name: string; image: string; @@ -115,6 +218,7 @@ export interface ContainerDefinition{ essential?: boolean; hostname?: string; links?: string[]; + logConfiguration?: LogConfiguration[]; memory?: number; memoryReservation?: number; privileged?: boolean; @@ -122,12 +226,35 @@ export interface ContainerDefinition{ user?: string; workingDirectory?: string } - // environment?: list of key-value; - // extraHosts?: hostEntry[]; - // healthCheck?: healthCheck; - // linuxParameters: linuxParam[]; - // logConfiguration: logConfig[]; - // mountPoints?: mountPoint[]; - // portMappings?: portMapping[]; - // ulimits?: ulimit[]; - // volumesFrom?: volumeFrom[]; +// environment?: list of key-value; +// extraHosts?: hostEntry[]; +// healthCheck?: healthCheck; +// linuxParameters: linuxParam[]; +// mountPoints?: mountPoint[]; +// portMappings?: portMapping[]; +// ulimits?: ulimit[]; +// volumesFrom?: volumeFrom[]; + +export interface PlacementConstraint { + expression?: string; + type: string; // PlacementConstraintType; +} + +// enum PlacementConstraintType{ +// DistinctInstance = "distinctInstance", +// MemberOf = "memberOf" +// } + +export interface Volume { + host?: Host; + name?: string; +} + +export interface Host { + sourcePath?: string; +} + +export interface LogConfiguration { + logDriver: string; + // options?: +} From 2d53d89f7480bfab41bc2295f4a871a244cac88f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Sep 2018 11:46:29 +0200 Subject: [PATCH 012/140] Indentation 4 -> 2 --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 204 ++++----- packages/@aws-cdk/aws-ecs/lib/service.ts | 146 +++--- .../@aws-cdk/aws-ecs/lib/task-definition.ts | 430 +++++++++--------- 3 files changed, 390 insertions(+), 390 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index bae9f5823afcd..47424688286cf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -4,45 +4,45 @@ import cdk = require('@aws-cdk/cdk'); import { cloudformation, ClusterArn} from './ecs.generated'; export interface ClusterProps { - /** - * A name for the cluster. - * - * @default CloudFormation-generated name - */ - clusterName?: string; - - /** - * The VPC where your ECS instances will be running - */ - vpc: ec2.VpcNetworkRef; - - /** - * Whether or not the containers can access the instance role - * - * @default false - */ - containersAccessInstanceRole?: boolean; + /** + * A name for the cluster. + * + * @default CloudFormation-generated name + */ + clusterName?: string; + + /** + * The VPC where your ECS instances will be running + */ + vpc: ec2.VpcNetworkRef; + + /** + * Whether or not the containers can access the instance role + * + * @default false + */ + containersAccessInstanceRole?: boolean; } // TODO: replace this with call to SSM, stored as "/aws/service/ecs/optimized-ami/amazon-linux/recommended" // https://raw.githubusercontent.com/awslabs/aws-cloudformation-templates/master/aws/services/ECS/EC2LaunchType/clusters/public-vpc.yml export const ECS_OPTIMIZED_AMI = new ec2.GenericLinuxImage({ - 'us-east-2': 'ami-028a9de0a7e353ed9', - 'us-east-1': 'ami-00129b193dc81bc31', - 'us-west-2': 'ami-00d4f478', - 'us-west-1': 'ami-0d438d09af26c9583', - 'eu-west-2': 'ami-a44db8c3', - 'eu-west-3': 'ami-07da674f0655ef4e1', - 'eu-west-1': 'ami-0af844a965e5738db', - 'eu-central-1': 'ami-0291ba887ba0d515f', - 'ap-northeast-2': 'ami-047d2a61f94f862dc', - 'ap-northeast-1': 'ami-0041c416aa23033a2', - 'ap-southeast-2': 'ami-0092e55c70015d8c3', - 'ap-southeast-1': 'ami-091bf462afdb02c60', - 'ca-central-1': 'ami-192fa27d', - 'ap-south-1': 'ami-0c179ca015d301829', - 'sa-east-1': 'ami-0018ff8ee48970ac3', - 'us-gov-west-1': 'ami-c6079ba7', + 'us-east-2': 'ami-028a9de0a7e353ed9', + 'us-east-1': 'ami-00129b193dc81bc31', + 'us-west-2': 'ami-00d4f478', + 'us-west-1': 'ami-0d438d09af26c9583', + 'eu-west-2': 'ami-a44db8c3', + 'eu-west-3': 'ami-07da674f0655ef4e1', + 'eu-west-1': 'ami-0af844a965e5738db', + 'eu-central-1': 'ami-0291ba887ba0d515f', + 'ap-northeast-2': 'ami-047d2a61f94f862dc', + 'ap-northeast-1': 'ami-0041c416aa23033a2', + 'ap-southeast-2': 'ami-0092e55c70015d8c3', + 'ap-southeast-1': 'ami-091bf462afdb02c60', + 'ca-central-1': 'ami-192fa27d', + 'ap-south-1': 'ami-0c179ca015d301829', + 'sa-east-1': 'ami-0018ff8ee48970ac3', + 'us-gov-west-1': 'ami-c6079ba7', }); // Needs to inherit from CloudFormationToken to make the string substitution @@ -52,74 +52,74 @@ export class ClusterName extends cdk.CloudFormationToken { export class Cluster extends cdk.Construct { - public readonly clusterArn: ClusterArn; - - public readonly clusterName: ClusterName; - - public readonly fleet: autoscaling.AutoScalingGroup; - - constructor(parent: cdk.Construct, name: string, props: ClusterProps) { - super(parent, name); - - const cluster = new cloudformation.ClusterResource(this, "Resource", {clusterName: props.clusterName}); - - this.clusterArn = cluster.clusterArn; - - this.clusterName = new ClusterName(cluster.ref); - - const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { - vpc: props.vpc, - instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), - machineImage: ECS_OPTIMIZED_AMI, - updateType: autoscaling.UpdateType.ReplacingUpdate - }); - - // Tie instances to cluster - fleet.addUserData(`echo ECS_CLUSTER=${this.clusterName} >> /etc/ecs/ecs.config`); - - if (!props.containersAccessInstanceRole) { - // Deny containers access to instance metadata service - // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html - fleet.addUserData('sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP'); - fleet.addUserData('sudo service iptables save'); - } - - // Note: if the fleet doesn't launch or doesn't register itself with - // ECS, *Cluster* stabilization will fail after timing our for an hour - // or so, because the *Service* doesn't have any running instances. - // During this time, you CANNOT DO ANYTHING ELSE WITH YOUR STACK. - // - // Apart from the weird relationship here between Cluster and Service - // (why is Cluster failing and not Service?), the experience is... - // - // NOT GREAT. - // - // Also, there's sort of a bidirectional dependency between Cluster and ASG: - // - // - ASG depends on Cluster to get the ClusterName (which needs to go into - // UserData). - // - Cluster depends on ASG to boot up, so the service is launched, so the - // Cluster can stabilize. - - // ECS instances must be able to do these things - // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html - fleet.addToRolePolicy(new cdk.PolicyStatement().addActions( - "ecs:CreateCluster", - "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", - "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", - "ecr:GetAuthorizationToken", - "ecr:BatchCheckLayerAvailability", - "ecr:GetDownloadUrlForLayer", - "ecr:BatchGetImage", - "logs:CreateLogStream", - "logs:PutLogEvents" - ).addAllResources()); // Conceivably we might do better than all resources and add targeted ARNs - - this.fleet = fleet; + public readonly clusterArn: ClusterArn; + public readonly clusterName: ClusterName; + + public readonly fleet: autoscaling.AutoScalingGroup; + + constructor(parent: cdk.Construct, name: string, props: ClusterProps) { + super(parent, name); + + const cluster = new cloudformation.ClusterResource(this, "Resource", {clusterName: props.clusterName}); + + this.clusterArn = cluster.clusterArn; + + this.clusterName = new ClusterName(cluster.ref); + + const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { + vpc: props.vpc, + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), + machineImage: ECS_OPTIMIZED_AMI, + updateType: autoscaling.UpdateType.ReplacingUpdate + }); + + // Tie instances to cluster + fleet.addUserData(`echo ECS_CLUSTER=${this.clusterName} >> /etc/ecs/ecs.config`); + + if (!props.containersAccessInstanceRole) { + // Deny containers access to instance metadata service + // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html + fleet.addUserData('sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP'); + fleet.addUserData('sudo service iptables save'); } + + // Note: if the fleet doesn't launch or doesn't register itself with + // ECS, *Cluster* stabilization will fail after timing our for an hour + // or so, because the *Service* doesn't have any running instances. + // During this time, you CANNOT DO ANYTHING ELSE WITH YOUR STACK. + // + // Apart from the weird relationship here between Cluster and Service + // (why is Cluster failing and not Service?), the experience is... + // + // NOT GREAT. + // + // Also, there's sort of a bidirectional dependency between Cluster and ASG: + // + // - ASG depends on Cluster to get the ClusterName (which needs to go into + // UserData). + // - Cluster depends on ASG to boot up, so the service is launched, so the + // Cluster can stabilize. + + // ECS instances must be able to do these things + // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html + fleet.addToRolePolicy(new cdk.PolicyStatement().addActions( + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "logs:CreateLogStream", + "logs:PutLogEvents" + ).addAllResources()); // Conceivably we might do better than all resources and add targeted ARNs + + this.fleet = fleet; + + } } diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts index 9aaffd7ff3a69..f8b038745c209 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -4,89 +4,89 @@ import { cloudformation } from './ecs.generated'; import { TaskDefinitionArn } from './task-definition'; export interface ServiceProps { - /** - * Cluster where service will be deployed - */ - cluster: ClusterName; // should be required? do we assume 'default' exists? + /** + * Cluster where service will be deployed + */ + cluster: ClusterName; // should be required? do we assume 'default' exists? - /** - * Task Definition used for running tasks in the service - */ - taskDefinition: TaskDefinitionArn; + /** + * Task Definition used for running tasks in the service + */ + taskDefinition: TaskDefinitionArn; - /** - * Number of desired copies of running tasks - * - * @default 1 - */ - desiredCount?: number; + /** + * Number of desired copies of running tasks + * + * @default 1 + */ + desiredCount?: number; - /** - * A name for the service. - * - * @default CloudFormation-generated name - */ - serviceName?: string; + /** + * A name for the service. + * + * @default CloudFormation-generated name + */ + serviceName?: string; - /** - * Whether the service is hosted in EC2 or Fargate - * - * @default EC2 - */ - launchType?: string; // maybe unnecessary if we have different ECS vs FG service + /** + * Whether the service is hosted in EC2 or Fargate + * + * @default EC2 + */ + launchType?: string; // maybe unnecessary if we have different ECS vs FG service - /** - * The maximum number of tasks, specified as a percentage of the Amazon ECS - * service's DesiredCount value, that can run in a service during a - * deployment. - * - * @default 200 - */ - maximumPercent?: number; + /** + * The maximum number of tasks, specified as a percentage of the Amazon ECS + * service's DesiredCount value, that can run in a service during a + * deployment. + * + * @default 200 + */ + maximumPercent?: number; - /** - * The minimum number of tasks, specified as a percentage of - * the Amazon ECS service's DesiredCount value, that must - * continue to run and remain healthy during a deployment. - * - * @default 50 - */ - minimumHealthyPercent?: number; + /** + * The minimum number of tasks, specified as a percentage of + * the Amazon ECS service's DesiredCount value, that must + * continue to run and remain healthy during a deployment. + * + * @default 50 + */ + minimumHealthyPercent?: number; - /** - * The name or ARN of an AWS Identity and Access Management (IAM) role that - * allows your Amazon ECS container agent to make calls to your load - * balancer. - */ - role?: string; + /** + * The name or ARN of an AWS Identity and Access Management (IAM) role that + * allows your Amazon ECS container agent to make calls to your load + * balancer. + */ + role?: string; - ///////// TBD /////////////////////////////// - // healthCheckGracePeriodSeconds?: number; // only needed with load balancers - // loadBalancers?: LoadBalancer[]; - // placementConstraints?: PlacementConstraint[]; - // placementStrategies?: PlacementStrategy[]; - // networkConfiguration?: NetworkConfiguration; - // serviceRegistries?: ServiceRegistry[]; - // - // platformVersion?: string; // FARGATE ONLY. default is LATEST. Other options: 1.2.0, 1.1.0, 1.0.0 - //////////////////////////////////////////// + ///////// TBD /////////////////////////////// + // healthCheckGracePeriodSeconds?: number; // only needed with load balancers + // loadBalancers?: LoadBalancer[]; + // placementConstraints?: PlacementConstraint[]; + // placementStrategies?: PlacementStrategy[]; + // networkConfiguration?: NetworkConfiguration; + // serviceRegistries?: ServiceRegistry[]; + // + // platformVersion?: string; // FARGATE ONLY. default is LATEST. Other options: 1.2.0, 1.1.0, 1.0.0 + //////////////////////////////////////////// } export class Service extends cdk.Construct { - constructor(parent: cdk.Construct, name: string, props: ServiceProps) { - super(parent, name); + constructor(parent: cdk.Construct, name: string, props: ServiceProps) { + super(parent, name); - new cloudformation.ServiceResource(this, "Service", { - cluster: props.cluster, - taskDefinition: props.taskDefinition, - desiredCount: props.desiredCount, - serviceName: props.serviceName, - launchType: props.launchType, - deploymentConfiguration: { - maximumPercent: props.maximumPercent, - minimumHealthyPercent: props.minimumHealthyPercent - }, - role: props.role, - }); - } + new cloudformation.ServiceResource(this, "Service", { + cluster: props.cluster, + taskDefinition: props.taskDefinition, + desiredCount: props.desiredCount, + serviceName: props.serviceName, + launchType: props.launchType, + deploymentConfiguration: { + maximumPercent: props.maximumPercent, + minimumHealthyPercent: props.minimumHealthyPercent + }, + role: props.role, + }); + } } diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 4b2f02d235ebf..06293831512c3 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -3,228 +3,228 @@ import { cloudformation } from './ecs.generated'; export interface TaskDefinitionProps { - containerDefinitions: ContainerDefinition[]; - - /** - * The number of cpu units used by the task. If using the EC2 launch type, - * this field is optional. Supported values are between 128 CPU units - * (0.125 vCPUs) and 10240 CPU units (10 vCPUs). If you are using the - * Fargate launch type, this field is required and you must use one of the - * following values, which determines your range of valid values for the - * memory parameter: - * 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB - * 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB - * 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - * 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments - * 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments - * - * @default 256 - */ - cpu?: string; - - /** - * The Amazon Resource Name (ARN) of the task execution role that - * containers in this task can assume. All containers in this task are - * granted the permissions that are specified in this role. - * - * Needed in Fargate to communicate with Cloudwatch Logs and ECR. - */ - executionRoleArn?: string; - - /** - * Namespace for task definition versions - * - * @default CloudFormation-generated name - */ - family?: string; - - /** - * The amount (in MiB) of memory used by the task. If using the EC2 launch - * type, this field is optional and any value can be used. If you are using - * the Fargate launch type, this field is required and you must use one of - * the following values, which determines your range of valid values for - * the cpu parameter: - * - * 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU) - * - * 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU) - * - * 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU) - * - * Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU) - * - * Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU) - * - * @default 512 - */ - memory?: string; - - /** - * The Docker networking mode to use for the containers in the task, such as none, bridge, or host. - * For Fargate or to use task networking, "awsvpc" mode is required. - * - * @default bridge - */ - networkMode?: string; - - /** - * An array of placement constraint objects to use for the task. You can - * specify a maximum of 10 constraints per task (this limit includes - * constraints in the task definition and those specified at run time). - * - * Not supported in Fargate. - */ - placementConstraints?: PlacementConstraint[]; - - /** - * Valid values include EC2 and FARGATE. - * - * @default EC2 - */ - // requiresCompatibilities?: string[]; // FARGATE or EC2 -- set on ECS TD vs FG TD - - /** - * The Amazon Resource Name (ARN) of an AWS Identity and Access Management - * (IAM) role that grants containers in the task permission to call AWS - * APIs on your behalf - */ - taskRoleArn?: string; - - /** - * See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide//task_definition_parameters.html#volumes - */ - volumes?: Volume[]; + containerDefinitions: ContainerDefinition[]; + + /** + * The number of cpu units used by the task. If using the EC2 launch type, + * this field is optional. Supported values are between 128 CPU units + * (0.125 vCPUs) and 10240 CPU units (10 vCPUs). If you are using the + * Fargate launch type, this field is required and you must use one of the + * following values, which determines your range of valid values for the + * memory parameter: + * 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB + * 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB + * 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB + * 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments + * 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments + * + * @default 256 + */ + cpu?: string; + + /** + * The Amazon Resource Name (ARN) of the task execution role that + * containers in this task can assume. All containers in this task are + * granted the permissions that are specified in this role. + * + * Needed in Fargate to communicate with Cloudwatch Logs and ECR. + */ + executionRoleArn?: string; + + /** + * Namespace for task definition versions + * + * @default CloudFormation-generated name + */ + family?: string; + + /** + * The amount (in MiB) of memory used by the task. If using the EC2 launch + * type, this field is optional and any value can be used. If you are using + * the Fargate launch type, this field is required and you must use one of + * the following values, which determines your range of valid values for + * the cpu parameter: + * + * 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU) + * + * 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU) + * + * 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU) + * + * Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU) + * + * Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU) + * + * @default 512 + */ + memory?: string; + + /** + * The Docker networking mode to use for the containers in the task, such as none, bridge, or host. + * For Fargate or to use task networking, "awsvpc" mode is required. + * + * @default bridge + */ + networkMode?: string; + + /** + * An array of placement constraint objects to use for the task. You can + * specify a maximum of 10 constraints per task (this limit includes + * constraints in the task definition and those specified at run time). + * + * Not supported in Fargate. + */ + placementConstraints?: PlacementConstraint[]; + + /** + * Valid values include EC2 and FARGATE. + * + * @default EC2 + */ + // requiresCompatibilities?: string[]; // FARGATE or EC2 -- set on ECS TD vs FG TD + + /** + * The Amazon Resource Name (ARN) of an AWS Identity and Access Management + * (IAM) role that grants containers in the task permission to call AWS + * APIs on your behalf + */ + taskRoleArn?: string; + + /** + * See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide//task_definition_parameters.html#volumes + */ + volumes?: Volume[]; } export class TaskDefinitionArn extends cdk.Token { } export class TaskDefinition extends cdk.Construct { - public readonly taskDefinitionArn: TaskDefinitionArn; - private readonly containerDefinitions: cloudformation.TaskDefinitionResource.ContainerDefinitionProperty[] = []; - private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; - private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; - - constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { - super(parent, name); - - props.containerDefinitions.forEach(cd => this.addContainer(cd)); - - if (props.placementConstraints) { - props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); - } - - if (props.volumes) { - props.volumes.forEach(v => this.addVolume(v)); - } - - const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { - containerDefinitions: new cdk.Token(() => this.containerDefinitions), - cpu: props.cpu, - executionRoleArn: props.executionRoleArn, - family: props.family, - memory: props.memory, - networkMode: props.networkMode, - placementConstraints: new cdk.Token(() => this.placementConstraints), - taskRoleArn: props.taskRoleArn - }); - - this.taskDefinitionArn = taskDef.ref; - } + public readonly taskDefinitionArn: TaskDefinitionArn; + private readonly containerDefinitions: cloudformation.TaskDefinitionResource.ContainerDefinitionProperty[] = []; + private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; + private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; - private addContainer(definition: ContainerDefinition) { - const cd = this.renderContainerDefininition(definition); - this.containerDefinitions.push(cd); - } + constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { + super(parent, name); - private addPlacementConstraint(constraint: PlacementConstraint) { - const pc = this.renderPlacementConstraint(constraint); - this.placementConstraints.push(pc); - } - - private addVolume(volume: Volume) { - // const v = this.renderVolume(volume); - this.volumes.push(volume); - } + props.containerDefinitions.forEach(cd => this.addContainer(cd)); - // Populates task definition with container definition - private renderContainerDefininition(definition: ContainerDefinition): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { - // const logConfigs = this.renderLogConfiguration(definition.logConfiguration); // what to do if undefined? - - return { - name: definition.name, - image: definition.image, - command: definition.command, - cpu: definition.cpu, - disableNetworking: definition.disableNetworking, - dnsSearchDomains: definition.dnsSearchDomains, - dnsServers: definition.dnsServers, - // dockerLabels: definition.dockerLabels, - dockerSecurityOptions: definition.dockerSecurityOptions, - entryPoint: definition.entryPoint, - essential: definition.essential, - hostname: definition.hostname, - links: definition.links, - // logConfiguration: logConfigs, // only set if passed in? - memory: definition.memory, - memoryReservation: definition.memoryReservation, - privileged: definition.privileged, - readonlyRootFilesystem: definition.readonlyRootFilesystem, - user: definition.user, - workingDirectory: definition.workingDirectory - }; + if (props.placementConstraints) { + props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); } - private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { - return { - type: pc.type, - expression: pc.expression - }; + if (props.volumes) { + props.volumes.forEach(v => this.addVolume(v)); } - // private renderLogConfiguration(lc: LogConfiguration[]): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty.LogConfiguration[] { - // return { - // logDriver: lc.logDriver, - // options: lc.options - // }; - // } - - // private renderVolume(volume: Volume): cloudformation.TaskDefinitionResource.VolumeProperty { - // return { - // host: this.renderHost(volume.host), - // name: volume.name - // }; - // } - - // private renderHost(host: Host): cloudformation.TaskDefinitionResource.VolumeProperty.Host { - // return { - // sourcePath: host.sourcePath - // } - // } + const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { + containerDefinitions: new cdk.Token(() => this.containerDefinitions), + cpu: props.cpu, + executionRoleArn: props.executionRoleArn, + family: props.family, + memory: props.memory, + networkMode: props.networkMode, + placementConstraints: new cdk.Token(() => this.placementConstraints), + taskRoleArn: props.taskRoleArn + }); + + this.taskDefinitionArn = taskDef.ref; + } + + private addContainer(definition: ContainerDefinition) { + const cd = this.renderContainerDefininition(definition); + this.containerDefinitions.push(cd); + } + + private addPlacementConstraint(constraint: PlacementConstraint) { + const pc = this.renderPlacementConstraint(constraint); + this.placementConstraints.push(pc); + } + + private addVolume(volume: Volume) { + // const v = this.renderVolume(volume); + this.volumes.push(volume); + } + + // Populates task definition with container definition + private renderContainerDefininition(definition: ContainerDefinition): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { + // const logConfigs = this.renderLogConfiguration(definition.logConfiguration); // what to do if undefined? + + return { + name: definition.name, + image: definition.image, + command: definition.command, + cpu: definition.cpu, + disableNetworking: definition.disableNetworking, + dnsSearchDomains: definition.dnsSearchDomains, + dnsServers: definition.dnsServers, + // dockerLabels: definition.dockerLabels, + dockerSecurityOptions: definition.dockerSecurityOptions, + entryPoint: definition.entryPoint, + essential: definition.essential, + hostname: definition.hostname, + links: definition.links, + // logConfiguration: logConfigs, // only set if passed in? + memory: definition.memory, + memoryReservation: definition.memoryReservation, + privileged: definition.privileged, + readonlyRootFilesystem: definition.readonlyRootFilesystem, + user: definition.user, + workingDirectory: definition.workingDirectory + }; + } + + private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { + return { + type: pc.type, + expression: pc.expression + }; + } + + // private renderLogConfiguration(lc: LogConfiguration[]): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty.LogConfiguration[] { + // return { + // logDriver: lc.logDriver, + // options: lc.options + // }; + // } + + // private renderVolume(volume: Volume): cloudformation.TaskDefinitionResource.VolumeProperty { + // return { + // host: this.renderHost(volume.host), + // name: volume.name + // }; + // } + + // private renderHost(host: Host): cloudformation.TaskDefinitionResource.VolumeProperty.Host { + // return { + // sourcePath: host.sourcePath + // } + // } } export interface ContainerDefinition { - name: string; - image: string; - - command?: string[]; - cpu?: number; - disableNetworking?: boolean; - dnsSearchDomains?: string[]; - dnsServers?: string[]; - dockerLabels?: string[]; - dockerSecurityOptions?: string[]; - entryPoint?: string[]; - essential?: boolean; - hostname?: string; - links?: string[]; - logConfiguration?: LogConfiguration[]; - memory?: number; - memoryReservation?: number; - privileged?: boolean; - readonlyRootFilesystem?: boolean; - user?: string; - workingDirectory?: string + name: string; + image: string; + + command?: string[]; + cpu?: number; + disableNetworking?: boolean; + dnsSearchDomains?: string[]; + dnsServers?: string[]; + dockerLabels?: string[]; + dockerSecurityOptions?: string[]; + entryPoint?: string[]; + essential?: boolean; + hostname?: string; + links?: string[]; + logConfiguration?: LogConfiguration[]; + memory?: number; + memoryReservation?: number; + privileged?: boolean; + readonlyRootFilesystem?: boolean; + user?: string; + workingDirectory?: string } // environment?: list of key-value; // extraHosts?: hostEntry[]; @@ -236,25 +236,25 @@ export interface ContainerDefinition { // volumesFrom?: volumeFrom[]; export interface PlacementConstraint { - expression?: string; - type: string; // PlacementConstraintType; + expression?: string; + type: string; // PlacementConstraintType; } // enum PlacementConstraintType{ -// DistinctInstance = "distinctInstance", -// MemberOf = "memberOf" +// DistinctInstance = "distinctInstance", +// MemberOf = "memberOf" // } export interface Volume { - host?: Host; - name?: string; + host?: Host; + name?: string; } export interface Host { - sourcePath?: string; + sourcePath?: string; } export interface LogConfiguration { - logDriver: string; - // options?: + logDriver: string; + // options?: } From dbdab8606b758a7fb3d668dfb22b53b307eeebd8 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Sep 2018 12:13:37 +0200 Subject: [PATCH 013/140] Fix type errors --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 9 ++++----- packages/@aws-cdk/aws-ecs/lib/index.ts | 2 +- packages/@aws-cdk/aws-ecs/lib/service.ts | 12 ++++++------ packages/@aws-cdk/aws-ecs/lib/task-definition.ts | 5 +---- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 47424688286cf..8e7e50f3681b8 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -1,7 +1,7 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); -import { cloudformation, ClusterArn} from './ecs.generated'; +import { cloudformation } from './ecs.generated'; export interface ClusterProps { /** @@ -52,9 +52,9 @@ export class ClusterName extends cdk.CloudFormationToken { export class Cluster extends cdk.Construct { - public readonly clusterArn: ClusterArn; + public readonly clusterArn: string; - public readonly clusterName: ClusterName; + public readonly clusterName: string; public readonly fleet: autoscaling.AutoScalingGroup; @@ -64,8 +64,7 @@ export class Cluster extends cdk.Construct { const cluster = new cloudformation.ClusterResource(this, "Resource", {clusterName: props.clusterName}); this.clusterArn = cluster.clusterArn; - - this.clusterName = new ClusterName(cluster.ref); + this.clusterName = cluster.ref; const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { vpc: props.vpc, diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 4a2b652775532..53babeb50b7a4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -3,4 +3,4 @@ export * from './service'; export * from './task-definition'; // AWS::ECS CloudFormation Resources: -export {ClusterName, TaskDefinitionArn} from './ecs.generated'; +export * from './ecs.generated'; diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts index f8b038745c209..843532dc07c8d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -1,18 +1,18 @@ import cdk = require('@aws-cdk/cdk'); -import { ClusterName } from './cluster'; +import { Cluster } from './cluster'; import { cloudformation } from './ecs.generated'; -import { TaskDefinitionArn } from './task-definition'; +import { TaskDefinition } from './task-definition'; export interface ServiceProps { /** * Cluster where service will be deployed */ - cluster: ClusterName; // should be required? do we assume 'default' exists? + cluster: Cluster; // should be required? do we assume 'default' exists? /** * Task Definition used for running tasks in the service */ - taskDefinition: TaskDefinitionArn; + taskDefinition: TaskDefinition; /** * Number of desired copies of running tasks @@ -77,8 +77,8 @@ export class Service extends cdk.Construct { super(parent, name); new cloudformation.ServiceResource(this, "Service", { - cluster: props.cluster, - taskDefinition: props.taskDefinition, + cluster: props.cluster.clusterName, + taskDefinition: props.taskDefinition.taskDefinitionArn, desiredCount: props.desiredCount, serviceName: props.serviceName, launchType: props.launchType, diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 06293831512c3..55856da1372cf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -96,11 +96,8 @@ export interface TaskDefinitionProps { volumes?: Volume[]; } -export class TaskDefinitionArn extends cdk.Token { -} - export class TaskDefinition extends cdk.Construct { - public readonly taskDefinitionArn: TaskDefinitionArn; + public readonly taskDefinitionArn: string; private readonly containerDefinitions: cloudformation.TaskDefinitionResource.ContainerDefinitionProperty[] = []; private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; From ee6b427eb82a16c2a3d14ee1af9fe5444e64b488 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 28 Sep 2018 13:32:28 +0200 Subject: [PATCH 014/140] Use SSM parameter for ECS Optimized AMI --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 38 ++++++++---------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 8e7e50f3681b8..efea48df8b428 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -24,30 +24,18 @@ export interface ClusterProps { containersAccessInstanceRole?: boolean; } -// TODO: replace this with call to SSM, stored as "/aws/service/ecs/optimized-ami/amazon-linux/recommended" -// https://raw.githubusercontent.com/awslabs/aws-cloudformation-templates/master/aws/services/ECS/EC2LaunchType/clusters/public-vpc.yml -export const ECS_OPTIMIZED_AMI = new ec2.GenericLinuxImage({ - 'us-east-2': 'ami-028a9de0a7e353ed9', - 'us-east-1': 'ami-00129b193dc81bc31', - 'us-west-2': 'ami-00d4f478', - 'us-west-1': 'ami-0d438d09af26c9583', - 'eu-west-2': 'ami-a44db8c3', - 'eu-west-3': 'ami-07da674f0655ef4e1', - 'eu-west-1': 'ami-0af844a965e5738db', - 'eu-central-1': 'ami-0291ba887ba0d515f', - 'ap-northeast-2': 'ami-047d2a61f94f862dc', - 'ap-northeast-1': 'ami-0041c416aa23033a2', - 'ap-southeast-2': 'ami-0092e55c70015d8c3', - 'ap-southeast-1': 'ami-091bf462afdb02c60', - 'ca-central-1': 'ami-192fa27d', - 'ap-south-1': 'ami-0c179ca015d301829', - 'sa-east-1': 'ami-0018ff8ee48970ac3', - 'us-gov-west-1': 'ami-c6079ba7', -}); - -// Needs to inherit from CloudFormationToken to make the string substitution -// downstairs work. This is temporary, will go away in the near future. -export class ClusterName extends cdk.CloudFormationToken { +/** + * Construct a Linux machine image from the latest ECS Optimized AMI published in SSM + */ +export class EcsOptimizedAmi implements ec2.IMachineImageSource { + private static AmiParamterName = "/aws/service/ecs/optimized-ami/amazon-linux/recommended"; + + public getImage(parent: cdk.Construct): ec2.MachineImage { + const ssmProvider = new cdk.SSMParameterProvider(parent); + + const ami = ssmProvider.getString(EcsOptimizedAmi.AmiParamterName); + return new ec2.MachineImage(ami, new ec2.LinuxOS()); + } } export class Cluster extends cdk.Construct { @@ -69,7 +57,7 @@ export class Cluster extends cdk.Construct { const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { vpc: props.vpc, instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), - machineImage: ECS_OPTIMIZED_AMI, + machineImage: new EcsOptimizedAmi(), updateType: autoscaling.UpdateType.ReplacingUpdate }); From 2b16dad7634988e770631cf4bc09588efa5da03b Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Sep 2018 13:40:07 +0200 Subject: [PATCH 015/140] Move containerdefinition to its own file --- .../aws-ecs/lib/container-definition.ts | 37 ++++++++++++++++++ packages/@aws-cdk/aws-ecs/lib/index.ts | 2 + .../aws-ecs/lib/log-drivers/aws-log-driver.ts | 4 ++ .../@aws-cdk/aws-ecs/lib/task-definition.ts | 38 +------------------ 4 files changed, 44 insertions(+), 37 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/container-definition.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts new file mode 100644 index 0000000000000..6e571fe6a72c0 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -0,0 +1,37 @@ +export interface ContainerDefinition { + name: string; + image: string; + + command?: string[]; + cpu?: number; + disableNetworking?: boolean; + dnsSearchDomains?: string[]; + dnsServers?: string[]; + dockerLabels?: string[]; + dockerSecurityOptions?: string[]; + entryPoint?: string[]; + essential?: boolean; + hostname?: string; + links?: string[]; + logConfiguration?: LogConfiguration[]; + memory?: number; + memoryReservation?: number; + privileged?: boolean; + readonlyRootFilesystem?: boolean; + user?: string; + workingDirectory?: string +} + +export interface LogConfiguration { + logDriver: string; + // options?: +} + +// environment?: list of key-value; +// extraHosts?: hostEntry[]; +// healthCheck?: healthCheck; +// linuxParameters: linuxParam[]; +// mountPoints?: mountPoint[]; +// portMappings?: portMapping[]; +// ulimits?: ulimit[]; +// volumesFrom?: volumeFrom[]; diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 53babeb50b7a4..9658da2d11cde 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,6 +1,8 @@ export * from './cluster'; export * from './service'; export * from './task-definition'; +export * from './container-definition'; +export * from './log-drivers/aws-log-driver'; // AWS::ECS CloudFormation Resources: export * from './ecs.generated'; diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts new file mode 100644 index 0000000000000..ed581cac5580a --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts @@ -0,0 +1,4 @@ + +export class AwsLogDriver { + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 55856da1372cf..97113604a63cd 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/cdk'); +import { ContainerDefinition } from './container-definition'; import { cloudformation } from './ecs.generated'; export interface TaskDefinitionProps { @@ -200,38 +201,6 @@ export class TaskDefinition extends cdk.Construct { // } } -export interface ContainerDefinition { - name: string; - image: string; - - command?: string[]; - cpu?: number; - disableNetworking?: boolean; - dnsSearchDomains?: string[]; - dnsServers?: string[]; - dockerLabels?: string[]; - dockerSecurityOptions?: string[]; - entryPoint?: string[]; - essential?: boolean; - hostname?: string; - links?: string[]; - logConfiguration?: LogConfiguration[]; - memory?: number; - memoryReservation?: number; - privileged?: boolean; - readonlyRootFilesystem?: boolean; - user?: string; - workingDirectory?: string -} -// environment?: list of key-value; -// extraHosts?: hostEntry[]; -// healthCheck?: healthCheck; -// linuxParameters: linuxParam[]; -// mountPoints?: mountPoint[]; -// portMappings?: portMapping[]; -// ulimits?: ulimit[]; -// volumesFrom?: volumeFrom[]; - export interface PlacementConstraint { expression?: string; type: string; // PlacementConstraintType; @@ -250,8 +219,3 @@ export interface Volume { export interface Host { sourcePath?: string; } - -export interface LogConfiguration { - logDriver: string; - // options?: -} From ae881c2a9a9f4980fd924f80bca353c52d22b35f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 28 Sep 2018 15:31:35 +0200 Subject: [PATCH 016/140] Turning ContainerDefinition into a construct --- .../hello-cdk-ecs/index.ts | 25 +- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 23 +- .../aws-ecs/lib/container-definition.ts | 300 +++++++++++++++++- .../@aws-cdk/aws-ecs/lib/container-image.ts | 21 ++ packages/@aws-cdk/aws-ecs/lib/index.ts | 4 + .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 16 + .../aws-ecs/lib/log-drivers/aws-log-driver.ts | 89 +++++- .../aws-ecs/lib/log-drivers/log-driver.ts | 12 + .../@aws-cdk/aws-ecs/lib/task-definition.ts | 71 +---- packages/@aws-cdk/aws-ecs/package.json | 3 +- 10 files changed, 464 insertions(+), 100 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/container-image.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 08f3ba4220012..a51b9688b233e 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -31,27 +31,22 @@ class BonjourECS extends cdk.Stack { // with the application container itself // - autoscaling - application autoscaling (Fargate focused?) - const taskDef = new ecs.TaskDefinition(this, "MyTD", { + const taskDefinition = new ecs.TaskDefinition(this, "MyTD", { family: "ecs-task-definition", placementConstraints: [{ type: "distinctInstance" }], - containerDefinitions: [ - { - name: "web", - image: "amazon/amazon-ecs-sample", - cpu: 1024, - memory: 512, - essential: true - } - ] }); - new ecs.Service(this, "Service", { - cluster: cluster.clusterName, - taskDefinition: taskDef.taskDefinitionArn, - desiredCount: 1, - }); + taskDefinition.addContainer(new ecs.ContainerDefinition(this, 'Def', { + name: "web", + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + cpu: 1024, + memoryMiB: 512, + essential: true + })); + + cluster.runService(taskDefinition); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index efea48df8b428..1ad78b021a536 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -2,6 +2,8 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation } from './ecs.generated'; +import { Service } from './service'; +import { TaskDefinition } from './task-definition'; export interface ClusterProps { /** @@ -49,12 +51,12 @@ export class Cluster extends cdk.Construct { constructor(parent: cdk.Construct, name: string, props: ClusterProps) { super(parent, name); - const cluster = new cloudformation.ClusterResource(this, "Resource", {clusterName: props.clusterName}); + const cluster = new cloudformation.ClusterResource(this, 'Resource', {clusterName: props.clusterName}); this.clusterArn = cluster.clusterArn; this.clusterName = cluster.ref; - const fleet = new autoscaling.AutoScalingGroup(this, 'MyASG', { + const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'AutoScalingGroup', { vpc: props.vpc, instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), machineImage: new EcsOptimizedAmi(), @@ -62,13 +64,13 @@ export class Cluster extends cdk.Construct { }); // Tie instances to cluster - fleet.addUserData(`echo ECS_CLUSTER=${this.clusterName} >> /etc/ecs/ecs.config`); + autoScalingGroup.addUserData(`echo ECS_CLUSTER=${this.clusterName} >> /etc/ecs/ecs.config`); if (!props.containersAccessInstanceRole) { // Deny containers access to instance metadata service // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html - fleet.addUserData('sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP'); - fleet.addUserData('sudo service iptables save'); + autoScalingGroup.addUserData('sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP'); + autoScalingGroup.addUserData('sudo service iptables save'); } // Note: if the fleet doesn't launch or doesn't register itself with @@ -90,7 +92,7 @@ export class Cluster extends cdk.Construct { // ECS instances must be able to do these things // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html - fleet.addToRolePolicy(new cdk.PolicyStatement().addActions( + autoScalingGroup.addToRolePolicy(new cdk.PolicyStatement().addActions( "ecs:CreateCluster", "ecs:DeregisterContainerInstance", "ecs:DiscoverPollEndpoint", @@ -106,7 +108,14 @@ export class Cluster extends cdk.Construct { "logs:PutLogEvents" ).addAllResources()); // Conceivably we might do better than all resources and add targeted ARNs - this.fleet = fleet; + this.fleet = autoScalingGroup; + } + public runService(taskDefinition: TaskDefinition): Service { + return new Service(this, `${taskDefinition.family}Service`, { + cluster: this, + taskDefinition, + // FIXME: additional props? Or set on Service object? + }); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 6e571fe6a72c0..de97fe38048e6 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -1,37 +1,307 @@ -export interface ContainerDefinition { +import cdk = require('@aws-cdk/cdk'); +import { ContainerImage } from './container-image'; +import { cloudformation } from './ecs.generated'; +import { LinuxParameters } from './linux-parameters'; +import { LogDriver } from './log-drivers/log-driver'; + +export interface ContainerDefinitionProps { + /** + * A name for the container. + */ name: string; - image: string; + /** + * The image to use for a container. + * + * You can use images in the Docker Hub registry or specify other + * repositories (repository-url/image:tag). + */ + image: ContainerImage; + + /** + * The CMD value to pass to the container. + * + * @default CMD value built into container image + */ command?: string[]; + + /** + * The minimum number of CPU units to reserve for the container. + */ cpu?: number; + + /** + * Indicates whether networking is disabled within the container. + * + * @default false + */ disableNetworking?: boolean; + + /** + * A list of DNS search domains that are provided to the container. + * + * @default No search domains + */ dnsSearchDomains?: string[]; + + /** + * A list of DNS servers that Amazon ECS provides to the container. + * + * @default Default DNS servers + */ dnsServers?: string[]; - dockerLabels?: string[]; + + /** + * A key-value map of labels for the container. + * + * @default No labels + */ + dockerLabels?: {[key: string]: string }; + + /** + * A list of custom labels for SELinux and AppArmor multi-level security systems. + * + * @default No security labels + */ dockerSecurityOptions?: string[]; + + /** + * The ENTRYPOINT value to pass to the container. + * + * @see https://docs.docker.com/engine/reference/builder/#entrypoint + * @default Entry point configured in container + */ entryPoint?: string[]; + + /** + * The environment variables to pass to the container. + * + * @default No environment variables + */ + environment?: {[key: string]: string}; + + /** + * Indicates whether the task stops if this container fails. + * + * If you specify true and the container fails, all other containers in the + * task stop. If you specify false and the container fails, none of the other + * containers in the task is affected. This value is true by default. + * + * You must have at least one essential container in a task. + * + * @default false + */ essential?: boolean; + + /** + * A list of hostnames and IP address mappings to append to the /etc/hosts file on the container. + * + * @default No extra hosts + */ + extraHosts?: {[name: string]: string}; + + /** + * Container health check. + * + * @default Health check configuration from container + */ + healthCheck?: HealthCheck; + + /** + * The name that Docker uses for the container hostname. + * + * @default Automatic hostname + */ hostname?: string; - links?: string[]; - logConfiguration?: LogConfiguration[]; - memory?: number; - memoryReservation?: number; + + /** + * The hard limit (in MiB) of memory to present to the container. + * + * If your container attempts to exceed the allocated memory, the container + * is terminated. + */ + memoryMiB?: number; + + /** + * The soft limit (in MiB) of memory to reserve for the container. + * + * When system memory is under contention, Docker attempts to keep the + * container memory within the limit. If the container requires more memory, + * it can consume up to the value specified by the Memory property or all of + * the available memory on the container instance—whichever comes first. + */ + memoryReservationMiB?: number; + + /** + * Indicates whether the container is given full access to the host container instance. + * + * @default false + */ privileged?: boolean; + + /** + * Indicates whether the container's root file system is mounted as read only. + * + * @default false + */ readonlyRootFilesystem?: boolean; + + /** + * The user name to use inside the container. + * + * @default root + */ user?: string; - workingDirectory?: string + + /** + * The working directory in the container to run commands in. + * + * @default / + */ + workingDirectory?: string; + + /** + * Configures a custom log driver for the container. + */ + logging?: LogDriver; } -export interface LogConfiguration { - logDriver: string; - // options?: +export class ContainerDefinition extends cdk.Construct { + public readonly name: string; + + public readonly linuxParameters = new LinuxParameters(); + + private readonly links = new Array(); + + constructor(parent: cdk.Construct, id: string, private readonly props: ContainerDefinitionProps) { + super(parent, id); + this.name = props.name; + props.image.bind(this); + } + + public addLink(container: ContainerDefinition, alias?: string) { + if (alias !== undefined) { + this.links.push(`${container.name}:${alias}`); + } else { + this.links.push(`${container.name}`); + } + } + + public toContainerDefinitionJson(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { + return { + command: this.props.command, + cpu: this.props.cpu, + disableNetworking: this.props.disableNetworking, + dnsSearchDomains: this.props.dnsSearchDomains, + dnsServers: this.props.dnsServers, + dockerLabels: this.props.dockerLabels, + dockerSecurityOptions: this.props.dockerSecurityOptions, + entryPoint: this.props.entryPoint, + essential: this.props.essential, + hostname: this.props.hostname, + image: this.props.image.imageName, + memory: this.props.memoryMiB, + memoryReservation: this.props.memoryReservationMiB, + mountPoints: [], // FIXME + name: this.props.name, + portMappings: [], // FIXME + privileged: this.props.privileged, + readonlyRootFilesystem: this.props.readonlyRootFilesystem, + repositoryCredentials: undefined, // FIXME + ulimits: [], // FIXME + user: this.props.user, + volumesFrom: [], // FIXME + workingDirectory: this.props.workingDirectory, + logConfiguration: this.props.logging && this.props.logging.toLogDriverJson(), + environment: this.props.environment && renderKV(this.props.environment, 'name', 'value'), + extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'), + healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), + links: this.links, + linuxParameters: this.linuxParameters.toLinuxParametersJson(), + }; + } +} + +/** + * Container health check configuration + */ +export interface HealthCheck { + /** + * Command to run, as the binary path and arguments. + * + * If you use this form, you do not have to quote command-line arguments. + * + * Exactly one of command and shellCommand must be supplied. + */ + command?: string[]; + + /** + * Command to run, as a shell command + * + * Exactly one of command and shellCommand must be supplied. + */ + shellCommand?: string; + + /** + * Time period in seconds between each health check execution. + * + * You may specify between 5 and 300 seconds. + * + * @default 30 + */ + intervalSeconds?: number; + + /** + * Number of times to retry a failed health check before the container is considered unhealthy. + * + * You may specify between 1 and 10 retries. + * + * @default 3 + */ + retries?: number; + + /** + * Grace period after startup before failed health checks count. + * + * You may specify between 0 and 300 seconds. + * + * @default No start period + */ + startPeriod?: number; + + /** + * The time period in seconds to wait for a health check to succeed before it is considered a failure. + * + * You may specify between 2 and 60 seconds. + * + * @default 5 + */ + timeout?: number; } -// environment?: list of key-value; -// extraHosts?: hostEntry[]; -// healthCheck?: healthCheck; -// linuxParameters: linuxParam[]; // mountPoints?: mountPoint[]; // portMappings?: portMapping[]; // ulimits?: ulimit[]; // volumesFrom?: volumeFrom[]; + +function renderKV(env: {[key: string]: string}, keyName: string, valueName: string): any { + const ret = []; + for (const [key, value] of Object.entries(env)) { + ret.push({ [keyName]: key, [valueName]: value }); + } + return ret; +} + +function renderHealthCheck(hc: HealthCheck): cloudformation.TaskDefinitionResource.HealthCheckProperty { + if ((hc.command === undefined) === (hc.shellCommand === undefined)) { + throw new Error(`Exactly one of 'command' and 'shellCommand' must be supplied.`); + } + + return { + command: hc.command !== undefined ? ['CMD'].concat(hc.command) : ['CMD-SHELL', hc.shellCommand!], + interval: hc.intervalSeconds, + retries: hc.retries, + startPeriod: hc.startPeriod, + timeout: hc.timeout + }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/container-image.ts b/packages/@aws-cdk/aws-ecs/lib/container-image.ts new file mode 100644 index 0000000000000..25ecc9fb5590a --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/container-image.ts @@ -0,0 +1,21 @@ +import { ContainerDefinition } from './container-definition'; + +export abstract class ContainerImage { + public abstract readonly imageName: string; + public abstract bind(containerDefinition: ContainerDefinition): void; +} + +export class DockerHub { + public static image(name: string): ContainerImage { + return new DockerHubImage(name); + } +} + +class DockerHubImage { + constructor(public readonly imageName: string) { + } + + public bind(_containerDefinition: ContainerDefinition): void { + // Nothing + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 9658da2d11cde..58a43c2e84d3f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -2,6 +2,10 @@ export * from './cluster'; export * from './service'; export * from './task-definition'; export * from './container-definition'; +export * from './container-image'; +export * from './linux-parameters'; + +export * from './log-drivers/log-driver'; export * from './log-drivers/aws-log-driver'; // AWS::ECS CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts new file mode 100644 index 0000000000000..b9ea35bb61317 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -0,0 +1,16 @@ +import { cloudformation } from './ecs.generated'; + +export class LinuxParameters { + + public addCapability() { + // FIXME + } + + public dropCapability() { + // FIXME + } + + public toLinuxParametersJson(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { + return {}; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts index ed581cac5580a..65b34e4b51737 100644 --- a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts @@ -1,4 +1,91 @@ +import logs = require('@aws-cdk/aws-logs'); +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from '../ecs.generated'; +import { LogDriver } from "./log-driver"; -export class AwsLogDriver { +/** + * Properties for defining a new AWS Log Driver + */ +export interface AwsLogDriverProps { + /** + * Prefix for the log streams + * + * The awslogs-stream-prefix option allows you to associate a log stream + * with the specified prefix, the container name, and the ID of the Amazon + * ECS task to which the container belongs. If you specify a prefix with + * this option, then the log stream takes the following format: + * + * prefix-name/container-name/ecs-task-id + */ + streamPrefix: string; + /** + * The log group to log to + * + * @default A log group is automatically created + */ + logGroup?: logs.LogGroupRef; + + /** + * This option defines a multiline start pattern in Python strftime format. + * + * A log message consists of a line that matches the pattern and any + * following lines that don’t match the pattern. Thus the matched line is + * the delimiter between log messages. + */ + datetimeFormat?: string; + + /** + * This option defines a multiline start pattern using a regular expression. + * + * A log message consists of a line that matches the pattern and any + * following lines that don’t match the pattern. Thus the matched line is + * the delimiter between log messages. + */ + multilinePattern?: string; +} + +/** + * A log driver that will log to an AWS Log Group + */ +export class AwsLogDriver extends LogDriver { + /** + * The log group that the logs will be sent to + */ + public readonly logGroup: logs.LogGroupRef; + + constructor(parent: cdk.Construct, id: string, private readonly props: AwsLogDriverProps) { + super(parent, id); + this.logGroup = props.logGroup || new logs.LogGroup(this, 'LogGroup', { + retentionDays: 365, + }); + } + + /** + * Return the log driver CloudFormation JSON + */ + public toLogDriverJson(): cloudformation.TaskDefinitionResource.LogConfigurationProperty { + return { + logDriver: 'awslogs', + options: removeEmpty({ + 'awslogs-group': this.logGroup.logGroupName, + 'awslogs-stream-prefix': this.props.streamPrefix, + 'awslogs-region': `${new cdk.AwsRegion()}`, + 'awslogs-datetime-format': this.props.datetimeFormat, + 'awslogs-multiline-pattern': this.props.multilinePattern, + }), + }; + } +} + +/** + * Remove undefined values from a dictionary + */ +function removeEmpty(x: {[key: string]: (T | undefined)}): {[key: string]: T} { + for (const key of Object.keys(x)) { + if (!x[key]) { + delete x[key]; + } + } + return x as any; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts new file mode 100644 index 0000000000000..2b8e9c5882d2f --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts @@ -0,0 +1,12 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from '../ecs.generated'; + +/** + * Base class for log drivers + */ +export abstract class LogDriver extends cdk.Construct { + /** + * Return the log driver CloudFormation JSON + */ + public abstract toLogDriverJson(): cloudformation.TaskDefinitionResource.LogConfigurationProperty; +} diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 97113604a63cd..b6a2f77ea1054 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -3,9 +3,6 @@ import { ContainerDefinition } from './container-definition'; import { cloudformation } from './ecs.generated'; export interface TaskDefinitionProps { - - containerDefinitions: ContainerDefinition[]; - /** * The number of cpu units used by the task. If using the EC2 launch type, * this field is optional. Supported values are between 128 CPU units @@ -35,7 +32,7 @@ export interface TaskDefinitionProps { /** * Namespace for task definition versions * - * @default CloudFormation-generated name + * @default Automatically generated name */ family?: string; @@ -58,7 +55,7 @@ export interface TaskDefinitionProps { * * @default 512 */ - memory?: string; + memoryMiB?: string; /** * The Docker networking mode to use for the containers in the task, such as none, bridge, or host. @@ -98,15 +95,16 @@ export interface TaskDefinitionProps { } export class TaskDefinition extends cdk.Construct { + public readonly family: string; public readonly taskDefinitionArn: string; - private readonly containerDefinitions: cloudformation.TaskDefinitionResource.ContainerDefinitionProperty[] = []; + private readonly containerDefinitions = new Array(); private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { super(parent, name); - props.containerDefinitions.forEach(cd => this.addContainer(cd)); + this.family = props.family || this.uniqueId; if (props.placementConstraints) { props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); @@ -117,11 +115,11 @@ export class TaskDefinition extends cdk.Construct { } const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { - containerDefinitions: new cdk.Token(() => this.containerDefinitions), + containerDefinitions: new cdk.Token(() => this.containerDefinitions.map(x => x.toContainerDefinitionJson())), cpu: props.cpu, executionRoleArn: props.executionRoleArn, - family: props.family, - memory: props.memory, + family: this.family, + memory: props.memoryMiB, networkMode: props.networkMode, placementConstraints: new cdk.Token(() => this.placementConstraints), taskRoleArn: props.taskRoleArn @@ -130,9 +128,8 @@ export class TaskDefinition extends cdk.Construct { this.taskDefinitionArn = taskDef.ref; } - private addContainer(definition: ContainerDefinition) { - const cd = this.renderContainerDefininition(definition); - this.containerDefinitions.push(cd); + public addContainer(container: ContainerDefinition) { + this.containerDefinitions.push(container); } private addPlacementConstraint(constraint: PlacementConstraint) { @@ -145,60 +142,12 @@ export class TaskDefinition extends cdk.Construct { this.volumes.push(volume); } - // Populates task definition with container definition - private renderContainerDefininition(definition: ContainerDefinition): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { - // const logConfigs = this.renderLogConfiguration(definition.logConfiguration); // what to do if undefined? - - return { - name: definition.name, - image: definition.image, - command: definition.command, - cpu: definition.cpu, - disableNetworking: definition.disableNetworking, - dnsSearchDomains: definition.dnsSearchDomains, - dnsServers: definition.dnsServers, - // dockerLabels: definition.dockerLabels, - dockerSecurityOptions: definition.dockerSecurityOptions, - entryPoint: definition.entryPoint, - essential: definition.essential, - hostname: definition.hostname, - links: definition.links, - // logConfiguration: logConfigs, // only set if passed in? - memory: definition.memory, - memoryReservation: definition.memoryReservation, - privileged: definition.privileged, - readonlyRootFilesystem: definition.readonlyRootFilesystem, - user: definition.user, - workingDirectory: definition.workingDirectory - }; - } - private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { return { type: pc.type, expression: pc.expression }; } - - // private renderLogConfiguration(lc: LogConfiguration[]): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty.LogConfiguration[] { - // return { - // logDriver: lc.logDriver, - // options: lc.options - // }; - // } - - // private renderVolume(volume: Volume): cloudformation.TaskDefinitionResource.VolumeProperty { - // return { - // host: this.renderHost(volume.host), - // name: volume.name - // }; - // } - - // private renderHost(host: Host): cloudformation.TaskDefinitionResource.VolumeProperty.Host { - // return { - // sourcePath: host.sourcePath - // } - // } } export interface PlacementConstraint { diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 87faffc994ed5..2b5d170a1a2be 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -60,7 +60,8 @@ "dependencies": { "@aws-cdk/cdk": "^0.10.0", "@aws-cdk/aws-autoscaling": "^0.10.0", - "@aws-cdk/aws-ec2": "^0.10.0" + "@aws-cdk/aws-ec2": "^0.10.0", + "@aws-cdk/aws-logs": "^0.10.0" }, "homepage": "https://github.com/awslabs/aws-cdk" } From 0672e6a06bb90ecddcff1cbb45826596b43ee463 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 10:10:17 +0200 Subject: [PATCH 017/140] Add enum for placement constraints --- .../cdk-examples-typescript/hello-cdk-ecs/index.ts | 2 +- packages/@aws-cdk/aws-ecs/lib/task-definition.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index a51b9688b233e..188097118d87d 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -34,7 +34,7 @@ class BonjourECS extends cdk.Stack { const taskDefinition = new ecs.TaskDefinition(this, "MyTD", { family: "ecs-task-definition", placementConstraints: [{ - type: "distinctInstance" + type: PlacementConstraintType.DistinctInstance }], }); diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index b6a2f77ea1054..2626360778ec2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -152,13 +152,13 @@ export class TaskDefinition extends cdk.Construct { export interface PlacementConstraint { expression?: string; - type: string; // PlacementConstraintType; + type: PlacementConstraintType; } -// enum PlacementConstraintType{ -// DistinctInstance = "distinctInstance", -// MemberOf = "memberOf" -// } +export enum PlacementConstraintType { + DistinctInstance = "distinctInstance", + MemberOf = "memberOf" +} export interface Volume { host?: Host; From 4f93a5b16cb25bc19fc93c804c609a7a9690b848 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 10:20:40 +0200 Subject: [PATCH 018/140] Rename fleet to autoScalingGroup --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 1ad78b021a536..4dd934f97b2a0 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -46,7 +46,7 @@ export class Cluster extends cdk.Construct { public readonly clusterName: string; - public readonly fleet: autoscaling.AutoScalingGroup; + public readonly autoScalingGroup: autoscaling.AutoScalingGroup; constructor(parent: cdk.Construct, name: string, props: ClusterProps) { super(parent, name); @@ -73,7 +73,7 @@ export class Cluster extends cdk.Construct { autoScalingGroup.addUserData('sudo service iptables save'); } - // Note: if the fleet doesn't launch or doesn't register itself with + // Note: if the ASG doesn't launch or doesn't register itself with // ECS, *Cluster* stabilization will fail after timing our for an hour // or so, because the *Service* doesn't have any running instances. // During this time, you CANNOT DO ANYTHING ELSE WITH YOUR STACK. @@ -108,7 +108,7 @@ export class Cluster extends cdk.Construct { "logs:PutLogEvents" ).addAllResources()); // Conceivably we might do better than all resources and add targeted ARNs - this.fleet = autoScalingGroup; + this.autoScalingGroup = autoScalingGroup; } public runService(taskDefinition: TaskDefinition): Service { From 9b61b89328886289fc2494760fe35bd1d2060f98 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Oct 2018 10:38:53 +0200 Subject: [PATCH 019/140] Qualify "PlacementConstraintType" usage with package name --- examples/cdk-examples-typescript/hello-cdk-ecs/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 188097118d87d..3a2a0340b56e5 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -34,7 +34,7 @@ class BonjourECS extends cdk.Stack { const taskDefinition = new ecs.TaskDefinition(this, "MyTD", { family: "ecs-task-definition", placementConstraints: [{ - type: PlacementConstraintType.DistinctInstance + type: ecs.PlacementConstraintType.DistinctInstance }], }); From 98bf81ba0bb5e1fd3ce892768d1ccdece6b4b9f2 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Oct 2018 12:05:38 +0200 Subject: [PATCH 020/140] ECR can return itself as container image --- .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 21 +++++++++++++++++++ packages/@aws-cdk/aws-ecr/package.json | 1 + 2 files changed, 22 insertions(+) diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index 90b2dfd7e744d..8a40bcc2fe238 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -1,3 +1,4 @@ +import ecs = require('@aws-cdk/aws-ecs'); import cdk = require('@aws-cdk/cdk'); /** @@ -43,6 +44,13 @@ export abstract class RepositoryRef extends cdk.Construct { const parts = cdk.ArnUtils.parse(this.repositoryArn); return `${parts.account}.dkr.ecr.${parts.region}.amazonaws.com/${parts.resourceName}`; } + + /** + * Refer to a particular image tag from this repository + */ + public getImage(tag: string = "latest"): ecs.ContainerImage { + return new EcrImage(this, tag); + } } export interface RepositoryRefProps { @@ -66,3 +74,16 @@ class ImportedRepository extends RepositoryRef { // FIXME: Add annotation about policy we dropped on the floor } } + +class EcrImage extends ecs.ContainerImage { + public readonly imageName: string; + + constructor(repository: RepositoryRef, tag: string) { + super(); + this.imageName = `${repository.repositoryUri}:${tag}`; + } + + public bind(_containerDefinition: ecs.ContainerDefinition): void { + // Nothing, for now + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index f719c74e6cc9c..dee3612c30041 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -53,6 +53,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "^0.10.0", + "@aws-cdk/aws-ecs": "^0.10.0", "cdk-build-tools": "^0.10.0", "cdk-integ-tools": "^0.10.0", "cfn2ts": "^0.10.0", From ae1d525095c1c5d8cdd245c3cc75bce1e1c245af Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Oct 2018 12:25:09 +0200 Subject: [PATCH 021/140] Automatically generate execution and task roles --- .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 4 +- .../aws-ecs/lib/container-definition.ts | 13 ++++ packages/@aws-cdk/aws-ecs/lib/index.ts | 1 + .../@aws-cdk/aws-ecs/lib/task-definition.ts | 72 ++++++++++++++----- packages/@aws-cdk/aws-ecs/package.json | 1 + 5 files changed, 72 insertions(+), 19 deletions(-) diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index 8a40bcc2fe238..a38d332e60700 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -83,7 +83,7 @@ class EcrImage extends ecs.ContainerImage { this.imageName = `${repository.repositoryUri}:${tag}`; } - public bind(_containerDefinition: ecs.ContainerDefinition): void { - // Nothing, for now + public bind(containerDefinition: ecs.ContainerDefinition): void { + containerDefinition.useEcrImage(); } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index de97fe38048e6..7cff4163a37ff 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -173,6 +173,8 @@ export class ContainerDefinition extends cdk.Construct { private readonly links = new Array(); + private _usesEcrImages: boolean = false; + constructor(parent: cdk.Construct, id: string, private readonly props: ContainerDefinitionProps) { super(parent, id); this.name = props.name; @@ -187,6 +189,17 @@ export class ContainerDefinition extends cdk.Construct { } } + /** + * Mark this ContainerDefinition as using an ECR image + */ + public useEcrImage() { + this._usesEcrImages = true; + } + + public get usesEcrImages() { + return this._usesEcrImages; + } + public toContainerDefinitionJson(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { return { command: this.props.command, diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 58a43c2e84d3f..a4729352cf1a8 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -9,4 +9,5 @@ export * from './log-drivers/log-driver'; export * from './log-drivers/aws-log-driver'; // AWS::ECS CloudFormation Resources: +// export * from './ecs.generated'; diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 2626360778ec2..18f9ceb991705 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -1,3 +1,4 @@ +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { ContainerDefinition } from './container-definition'; import { cloudformation } from './ecs.generated'; @@ -20,15 +21,6 @@ export interface TaskDefinitionProps { */ cpu?: string; - /** - * The Amazon Resource Name (ARN) of the task execution role that - * containers in this task can assume. All containers in this task are - * granted the permissions that are specified in this role. - * - * Needed in Fargate to communicate with Cloudwatch Logs and ECR. - */ - executionRoleArn?: string; - /** * Namespace for task definition versions * @@ -82,11 +74,21 @@ export interface TaskDefinitionProps { // requiresCompatibilities?: string[]; // FARGATE or EC2 -- set on ECS TD vs FG TD /** - * The Amazon Resource Name (ARN) of an AWS Identity and Access Management - * (IAM) role that grants containers in the task permission to call AWS - * APIs on your behalf + * The IAM role assumed by the ECS agent. + * + * The role will be used to retrieve container images from ECR and + * create CloudWatch log groups. + * + * @default An execution role will be automatically created if you use ECR images in your task definition + */ + executionRole?: iam.Role; + + /** + * The IAM role assumable by your application code running inside the container + * + * @default A task role is automatically created for you */ - taskRoleArn?: string; + taskRole?: iam.Role; /** * See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide//task_definition_parameters.html#volumes @@ -100,6 +102,8 @@ export class TaskDefinition extends cdk.Construct { private readonly containerDefinitions = new Array(); private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; + private executionRole?: iam.Role; + private readonly taskRole: iam.Role; constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { super(parent, name); @@ -114,24 +118,46 @@ export class TaskDefinition extends cdk.Construct { props.volumes.forEach(v => this.addVolume(v)); } - const taskDef = new cloudformation.TaskDefinitionResource(this, "TaskDef", { + this.executionRole = props.executionRole; + + this.taskRole = props.taskRole || new iam.Role(this, 'TaskRole', { + assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { containerDefinitions: new cdk.Token(() => this.containerDefinitions.map(x => x.toContainerDefinitionJson())), cpu: props.cpu, - executionRoleArn: props.executionRoleArn, + executionRoleArn: new cdk.Token(() => this.executionRole && this.executionRole.roleArn), family: this.family, memory: props.memoryMiB, networkMode: props.networkMode, placementConstraints: new cdk.Token(() => this.placementConstraints), - taskRoleArn: props.taskRoleArn + taskRoleArn: this.taskRole.roleArn }); - this.taskDefinitionArn = taskDef.ref; + this.taskDefinitionArn = taskDef.taskDefinitionArn; } + /** + * Add a policy statement to the Task Role + */ + public addToRolePolicy(statement: cdk.PolicyStatement) { + this.taskRole.addToPolicy(statement); + } + + /** + * Add a container to this task + */ public addContainer(container: ContainerDefinition) { this.containerDefinitions.push(container); + if (container.usesEcrImages) { + this.generateExecutionRole(); + } } + /** + * Constrain where this task can be placed + */ private addPlacementConstraint(constraint: PlacementConstraint) { const pc = this.renderPlacementConstraint(constraint); this.placementConstraints.push(pc); @@ -148,6 +174,18 @@ export class TaskDefinition extends cdk.Construct { expression: pc.expression }; } + + /** + * Generate a default execution role that allows pulling from ECR + */ + private generateExecutionRole() { + if (!this.executionRole) { + this.executionRole = new iam.Role(this, 'ExecutionRole', { + assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + this.executionRole.attachManagedPolicy(new iam.AwsManagedPolicy("service-role/AmazonECSTaskExecutionRolePolicy").policyArn); + } + } } export interface PlacementConstraint { diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 2b5d170a1a2be..fd0c9c2813625 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -61,6 +61,7 @@ "@aws-cdk/cdk": "^0.10.0", "@aws-cdk/aws-autoscaling": "^0.10.0", "@aws-cdk/aws-ec2": "^0.10.0", + "@aws-cdk/aws-iam": "^0.10.0", "@aws-cdk/aws-logs": "^0.10.0" }, "homepage": "https://github.com/awslabs/aws-cdk" From 23072a978820f2ef0b7a2b0487f08b929ab01b6b Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Oct 2018 12:28:32 +0200 Subject: [PATCH 022/140] Make taskRole public --- packages/@aws-cdk/aws-ecs/lib/task-definition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 18f9ceb991705..fad04125f54ba 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -99,11 +99,11 @@ export interface TaskDefinitionProps { export class TaskDefinition extends cdk.Construct { public readonly family: string; public readonly taskDefinitionArn: string; + public readonly taskRole: iam.Role; private readonly containerDefinitions = new Array(); private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; private executionRole?: iam.Role; - private readonly taskRole: iam.Role; constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { super(parent, name); From e5cc7f253243c9122cd73cb0a25f6e5130ebec30 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 14:29:17 +0200 Subject: [PATCH 023/140] Remove shellCommand from healthCheck --- .../aws-ecs/lib/container-definition.ts | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 7cff4163a37ff..d789d3c0d2932 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -15,12 +15,15 @@ export interface ContainerDefinitionProps { * * You can use images in the Docker Hub registry or specify other * repositories (repository-url/image:tag). + * TODO: Update these to specify using classes of ContainerImage */ image: ContainerImage; /** * The CMD value to pass to the container. * + * If you provide a shell command as a single string, you have to quote command-line arguments. + * * @default CMD value built into container image */ command?: string[]; @@ -242,18 +245,9 @@ export interface HealthCheck { /** * Command to run, as the binary path and arguments. * - * If you use this form, you do not have to quote command-line arguments. - * - * Exactly one of command and shellCommand must be supplied. - */ - command?: string[]; - - /** - * Command to run, as a shell command - * - * Exactly one of command and shellCommand must be supplied. + * If you provide a shell command as a single string, you have to quote command-line arguments. */ - shellCommand?: string; + command: string[]; /** * Time period in seconds between each health check execution. @@ -306,15 +300,31 @@ function renderKV(env: {[key: string]: string}, keyName: string, valueName: stri } function renderHealthCheck(hc: HealthCheck): cloudformation.TaskDefinitionResource.HealthCheckProperty { - if ((hc.command === undefined) === (hc.shellCommand === undefined)) { - throw new Error(`Exactly one of 'command' and 'shellCommand' must be supplied.`); - } - return { - command: hc.command !== undefined ? ['CMD'].concat(hc.command) : ['CMD-SHELL', hc.shellCommand!], + command: getHealthCheckCommand(hc), interval: hc.intervalSeconds, retries: hc.retries, startPeriod: hc.startPeriod, timeout: hc.timeout }; -} \ No newline at end of file +} + +function getHealthCheckCommand(hc: HealthCheck): string[] { + let cmd = hc.command; + const hcCommand = new Array(); + + if (cmd.length === 0) { + throw new Error(`At least one argument must be supplied for health check command.`); + } + + if (cmd.length === 1) { + hcCommand.push('CMD-SHELL', cmd[0]); + return hcCommand; + } + + if (cmd[0] !== "CMD" || cmd[0] !== 'CMD-SHELL') { + hcCommand.push('CMD') + } + + return hcCommand.concat(cmd); +} From 047eed81b7fead441bf348c8b7f09bab90abc934 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 14:32:59 +0200 Subject: [PATCH 024/140] Rename toContainerDefinitionJson to renderContainerDefinition --- packages/@aws-cdk/aws-ecs/lib/container-definition.ts | 2 +- packages/@aws-cdk/aws-ecs/lib/task-definition.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index d789d3c0d2932..f19cbf78a1f38 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -203,7 +203,7 @@ export class ContainerDefinition extends cdk.Construct { return this._usesEcrImages; } - public toContainerDefinitionJson(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { + public renderContainerDefinition(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { return { command: this.props.command, cpu: this.props.cpu, diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index 18f9ceb991705..d42326fd42ae9 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -125,7 +125,7 @@ export class TaskDefinition extends cdk.Construct { }); const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { - containerDefinitions: new cdk.Token(() => this.containerDefinitions.map(x => x.toContainerDefinitionJson())), + containerDefinitions: new cdk.Token(() => this.containerDefinitions.map(x => x.renderContainerDefinition())), cpu: props.cpu, executionRoleArn: new cdk.Token(() => this.executionRole && this.executionRole.roleArn), family: this.family, From 65d3f790037c385a04f68512796c5b0e339c137a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Oct 2018 15:36:06 +0200 Subject: [PATCH 025/140] Some work on Service --- packages/@aws-cdk/aws-ecr/package.json | 4 +- .../aws-ecs/lib/container-definition.ts | 13 ++- packages/@aws-cdk/aws-ecs/lib/service.ts | 104 ++++++++++++++++-- .../@aws-cdk/aws-ecs/lib/task-definition.ts | 59 ++++++++-- packages/@aws-cdk/aws-ecs/package.json | 2 + 5 files changed, 161 insertions(+), 21 deletions(-) diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index dee3612c30041..f83da66b34989 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -53,14 +53,14 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "^0.10.0", - "@aws-cdk/aws-ecs": "^0.10.0", "cdk-build-tools": "^0.10.0", "cdk-integ-tools": "^0.10.0", "cfn2ts": "^0.10.0", "pkglint": "^0.10.0" }, "dependencies": { - "@aws-cdk/cdk": "^0.10.0" + "@aws-cdk/cdk": "^0.10.0", + "@aws-cdk/aws-ecs": "^0.10.0" }, "homepage": "https://github.com/awslabs/aws-cdk" } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 7cff4163a37ff..f8612b384764b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -85,11 +85,11 @@ export interface ContainerDefinitionProps { * * If you specify true and the container fails, all other containers in the * task stop. If you specify false and the container fails, none of the other - * containers in the task is affected. This value is true by default. + * containers in the task is affected. * * You must have at least one essential container in a task. * - * @default false + * @default true */ essential?: boolean; @@ -171,6 +171,8 @@ export class ContainerDefinition extends cdk.Construct { public readonly linuxParameters = new LinuxParameters(); + public readonly essential: boolean; + private readonly links = new Array(); private _usesEcrImages: boolean = false; @@ -178,6 +180,7 @@ export class ContainerDefinition extends cdk.Construct { constructor(parent: cdk.Construct, id: string, private readonly props: ContainerDefinitionProps) { super(parent, id); this.name = props.name; + this.essential = props.essential !== undefined ? props.essential : true; props.image.bind(this); } @@ -200,6 +203,10 @@ export class ContainerDefinition extends cdk.Construct { return this._usesEcrImages; } + public loadBalancerPort(_classicLB: boolean): number { + return 0; + } + public toContainerDefinitionJson(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { return { command: this.props.command, @@ -210,7 +217,7 @@ export class ContainerDefinition extends cdk.Construct { dockerLabels: this.props.dockerLabels, dockerSecurityOptions: this.props.dockerSecurityOptions, entryPoint: this.props.entryPoint, - essential: this.props.essential, + essential: this.essential, hostname: this.props.hostname, image: this.props.image.imageName, memory: this.props.memoryMiB, diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/service.ts index 843532dc07c8d..d03876bf485a5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/service.ts @@ -1,3 +1,7 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import elb = require('@aws-cdk/aws-elasticloadbalancing'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Cluster } from './cluster'; import { cloudformation } from './ecs.generated'; @@ -33,7 +37,7 @@ export interface ServiceProps { * * @default EC2 */ - launchType?: string; // maybe unnecessary if we have different ECS vs FG service + launchType?: LaunchType; /** * The maximum number of tasks, specified as a percentage of the Amazon ECS @@ -54,11 +58,18 @@ export interface ServiceProps { minimumHealthyPercent?: number; /** - * The name or ARN of an AWS Identity and Access Management (IAM) role that - * allows your Amazon ECS container agent to make calls to your load - * balancer. + * Role used by ECS agent to register containers with the Load Balancer + * + * @default A role will be created for you */ - role?: string; + role?: iam.Role; + + /** + * Time after startup to ignore unhealthy load balancer checks. + * + * @default ??? + */ + healthCheckGracePeriodSeconds?: number; ///////// TBD /////////////////////////////// // healthCheckGracePeriodSeconds?: number; // only needed with load balancers @@ -72,21 +83,98 @@ export interface ServiceProps { //////////////////////////////////////////// } -export class Service extends cdk.Construct { +export class Service extends cdk.Construct implements elb.ILoadBalancerTarget, + elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { + public readonly dependencyElements: cdk.IDependable[]; + public readonly connections: ec2.Connections; + private loadBalancers = new Array(); + private readonly taskDefinition: TaskDefinition; + private role?: iam.Role; + private readonly resource: cloudformation.ServiceResource; + constructor(parent: cdk.Construct, name: string, props: ServiceProps) { super(parent, name); - new cloudformation.ServiceResource(this, "Service", { + this.connections = new ec2.Connections({ + securityGroupRule: { + canInlineRule: false, + toEgressRuleJSON() { return {}; }, + toIngressRuleJSON() { return {}; }, + uniqueId: '' + }, + }); + + this.taskDefinition = props.taskDefinition; + if (!this.taskDefinition.defaultContainer) { + throw new Error('A TaskDefinition must have at least one essential container'); + } + + this.role = props.role; + + this.resource = new cloudformation.ServiceResource(this, "Service", { cluster: props.cluster.clusterName, taskDefinition: props.taskDefinition.taskDefinitionArn, desiredCount: props.desiredCount, serviceName: props.serviceName, launchType: props.launchType, + loadBalancers: new cdk.Token(() => this.loadBalancers), deploymentConfiguration: { maximumPercent: props.maximumPercent, minimumHealthyPercent: props.minimumHealthyPercent }, - role: props.role, + role: new cdk.Token(() => this.role && this.role.roleArn), }); + + this.dependencyElements = [this.resource]; } + + public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { + // FIXME: If Fargate then throw a helpful error + // FIXME: Security Groups + this.loadBalancers.push({ + loadBalancerName: loadBalancer.loadBalancerName, + containerName: this.taskDefinition.defaultContainer!.name, + containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(true), + }); + this.createLoadBalancerRole(); + } + + public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { + // FIXME: Security Groups + this.loadBalancers.push({ + targetGroupArn: targetGroup.targetGroupArn, + containerName: this.taskDefinition.defaultContainer!.name, + containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(false), + }); + this.createLoadBalancerRole(); + + return { targetType: elbv2.TargetType.SelfRegistering }; + } + + public attachToNetworkTargetGroup(targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { + this.loadBalancers.push({ + targetGroupArn: targetGroup.targetGroupArn, + containerName: this.taskDefinition.defaultContainer!.name, + containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(false), + }); + this.createLoadBalancerRole(); + + return { targetType: elbv2.TargetType.SelfRegistering }; + } + + public createLoadBalancerRole() { + if (!this.role) { + this.role = new iam.Role(this, 'Role', { + assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + this.role.attachManagedPolicy(new iam.AwsManagedPolicy('service-role/AmazonEC2ContainerServiceRole').policyArn); + this.resource.addDependency(this.role); + } + } + } + +export enum LaunchType { + EC2 = 'EC2', + Fargate = 'FARGATE' +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts index fad04125f54ba..77dde2643560b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/task-definition.ts @@ -50,12 +50,13 @@ export interface TaskDefinitionProps { memoryMiB?: string; /** - * The Docker networking mode to use for the containers in the task, such as none, bridge, or host. + * The Docker networking mode to use for the containers in the task. + * * For Fargate or to use task networking, "awsvpc" mode is required. * - * @default bridge + * @default NetworkMode.Bridge */ - networkMode?: string; + networkMode?: NetworkMode; /** * An array of placement constraint objects to use for the task. You can @@ -100,7 +101,17 @@ export class TaskDefinition extends cdk.Construct { public readonly family: string; public readonly taskDefinitionArn: string; public readonly taskRole: iam.Role; - private readonly containerDefinitions = new Array(); + public readonly networkMode: NetworkMode; + + /** + * Default container for this task + * + * Load balancers will send traffic to this container. The first + * essential container that is added to this task will become the default + * container. + */ + public defaultContainer?: ContainerDefinition; + private readonly containers = new Array(); private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; private executionRole?: iam.Role; @@ -109,6 +120,7 @@ export class TaskDefinition extends cdk.Construct { super(parent, name); this.family = props.family || this.uniqueId; + this.networkMode = props.networkMode || NetworkMode.Bridge; if (props.placementConstraints) { props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); @@ -125,12 +137,12 @@ export class TaskDefinition extends cdk.Construct { }); const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { - containerDefinitions: new cdk.Token(() => this.containerDefinitions.map(x => x.toContainerDefinitionJson())), + containerDefinitions: new cdk.Token(() => this.containers.map(x => x.toContainerDefinitionJson())), cpu: props.cpu, executionRoleArn: new cdk.Token(() => this.executionRole && this.executionRole.roleArn), family: this.family, memory: props.memoryMiB, - networkMode: props.networkMode, + networkMode: this.networkMode, placementConstraints: new cdk.Token(() => this.placementConstraints), taskRoleArn: this.taskRole.roleArn }); @@ -149,10 +161,13 @@ export class TaskDefinition extends cdk.Construct { * Add a container to this task */ public addContainer(container: ContainerDefinition) { - this.containerDefinitions.push(container); + this.containers.push(container); if (container.usesEcrImages) { this.generateExecutionRole(); } + if (this.defaultContainer === undefined && container.essential) { + this.defaultContainer = container; + } } /** @@ -188,6 +203,34 @@ export class TaskDefinition extends cdk.Construct { } } +/** + * The Docker networking mode to use for the containers in the task. + */ +export enum NetworkMode { + /** + * The task's containers do not have external connectivity and port mappings can't be specified in the container definition. + */ + None = 'none', + + /** + * The task utilizes Docker's built-in virtual network which runs inside each container instance. + */ + Bridge = 'bridge', + + /** + * The task is allocated an elastic network interface. + */ + AwsVpc = 'awsvpc', + + /** + * The task bypasses Docker's built-in virtual network and maps container ports directly to the EC2 instance's network interface directly. + * + * In this mode, you can't run multiple instantiations of the same task on a + * single container instance when port mappings are used. + */ + Host = 'host', +} + export interface PlacementConstraint { expression?: string; type: PlacementConstraintType; @@ -205,4 +248,4 @@ export interface Volume { export interface Host { sourcePath?: string; -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index fd0c9c2813625..e6c82d1e6e64a 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -61,6 +61,8 @@ "@aws-cdk/cdk": "^0.10.0", "@aws-cdk/aws-autoscaling": "^0.10.0", "@aws-cdk/aws-ec2": "^0.10.0", + "@aws-cdk/aws-elasticloadbalancing": "^0.10.0", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.10.0", "@aws-cdk/aws-iam": "^0.10.0", "@aws-cdk/aws-logs": "^0.10.0" }, From fb1753258edcd1ec9546a46ecb7565a80575c8a4 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 15:58:53 +0200 Subject: [PATCH 026/140] Separate Fargate and ECS clusters --- .../@aws-cdk/aws-ecs/lib/base/base-cluster.ts | 33 +++++++++ .../lib/{cluster.ts => ecs/ecs-cluster.ts} | 74 +++++++------------ .../aws-ecs/lib/fargate/fargate-cluster.ts | 12 +++ packages/@aws-cdk/aws-ecs/lib/index.ts | 4 +- 4 files changed, 73 insertions(+), 50 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts rename packages/@aws-cdk/aws-ecs/lib/{cluster.ts => ecs/ecs-cluster.ts} (76%) create mode 100644 packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts new file mode 100644 index 0000000000000..f416cf46351dd --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts @@ -0,0 +1,33 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from '../ecs.generated'; + +export interface BaseClusterProps { + /** + * A name for the cluster. + * + * @default CloudFormation-generated name + */ + clusterName?: string; + + /** + * The VPC where your ECS instances will be running + */ + vpc: ec2.VpcNetworkRef; +} + +export class BaseCluster extends cdk.Construct { + + public readonly clusterArn: string; + + public readonly clusterName: string; + + constructor(parent: cdk.Construct, name: string, props: BaseClusterProps) { + super(parent, name); + + const cluster = new cloudformation.ClusterResource(this, 'Resource', {clusterName: props.clusterName}); + + this.clusterArn = cluster.clusterArn; + this.clusterName = cluster.ref; + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts similarity index 76% rename from packages/@aws-cdk/aws-ecs/lib/cluster.ts rename to packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index 4dd934f97b2a0..a4231f12b07e1 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -1,23 +1,9 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); -import { cloudformation } from './ecs.generated'; -import { Service } from './service'; -import { TaskDefinition } from './task-definition'; - -export interface ClusterProps { - /** - * A name for the cluster. - * - * @default CloudFormation-generated name - */ - clusterName?: string; - - /** - * The VPC where your ECS instances will be running - */ - vpc: ec2.VpcNetworkRef; +import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; +export interface EcsClusterProps extends BaseClusterProps { /** * Whether or not the containers can access the instance role * @@ -26,35 +12,11 @@ export interface ClusterProps { containersAccessInstanceRole?: boolean; } -/** - * Construct a Linux machine image from the latest ECS Optimized AMI published in SSM - */ -export class EcsOptimizedAmi implements ec2.IMachineImageSource { - private static AmiParamterName = "/aws/service/ecs/optimized-ami/amazon-linux/recommended"; - - public getImage(parent: cdk.Construct): ec2.MachineImage { - const ssmProvider = new cdk.SSMParameterProvider(parent); - - const ami = ssmProvider.getString(EcsOptimizedAmi.AmiParamterName); - return new ec2.MachineImage(ami, new ec2.LinuxOS()); - } -} - -export class Cluster extends cdk.Construct { - - public readonly clusterArn: string; - - public readonly clusterName: string; - +export class EcsCluster extends BaseCluster { public readonly autoScalingGroup: autoscaling.AutoScalingGroup; - constructor(parent: cdk.Construct, name: string, props: ClusterProps) { - super(parent, name); - - const cluster = new cloudformation.ClusterResource(this, 'Resource', {clusterName: props.clusterName}); - - this.clusterArn = cluster.clusterArn; - this.clusterName = cluster.ref; + constructor(parent: cdk.Construct, name: string, props: EcsClusterProps) { + super(parent, name, props); const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'AutoScalingGroup', { vpc: props.vpc, @@ -111,11 +73,25 @@ export class Cluster extends cdk.Construct { this.autoScalingGroup = autoScalingGroup; } - public runService(taskDefinition: TaskDefinition): Service { - return new Service(this, `${taskDefinition.family}Service`, { - cluster: this, - taskDefinition, - // FIXME: additional props? Or set on Service object? - }); + // public runService(taskDefinition: EcsTaskDefinition): EcsService { + // return new Service(this, `${taskDefinition.family}Service`, { + // cluster: this, + // taskDefinition, + // // FIXME: additional props? Or set on Service object? + // }); + // } +} + +/** + * Construct a Linux machine image from the latest ECS Optimized AMI published in SSM + */ +export class EcsOptimizedAmi implements ec2.IMachineImageSource { + private static AmiParamterName = "/aws/service/ecs/optimized-ami/amazon-linux/recommended"; + + public getImage(parent: cdk.Construct): ec2.MachineImage { + const ssmProvider = new cdk.SSMParameterProvider(parent); + + const ami = ssmProvider.getString(EcsOptimizedAmi.AmiParamterName); + return new ec2.MachineImage(ami, new ec2.LinuxOS()); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts new file mode 100644 index 0000000000000..b928fb8be616f --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts @@ -0,0 +1,12 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; + +// tslint:disable-next-line:no-empty-interface +export interface FargateClusterProps extends BaseClusterProps { +} + +export class FargateCluster extends BaseCluster { + constructor(parent: cdk.Construct, name: string, props: FargateClusterProps) { + super(parent, name, props); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index a4729352cf1a8..ec49b26c2e0b9 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,4 +1,6 @@ -export * from './cluster'; +export * from './base/base-cluster'; +export * from './ecs/ecs-cluster'; +// export * from './fargate/fargate-cluster'; export * from './service'; export * from './task-definition'; export * from './container-definition'; From be8cb86d9ae04aeaa924e9d8e129f9e901181256 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 16:53:04 +0200 Subject: [PATCH 027/140] Separate ECS and Fargate Service --- .../lib/{service.ts => base/base-service.ts} | 70 ++++--------------- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 48 +++++++++++++ .../aws-ecs/lib/fargate/fargate-service.ts | 37 ++++++++++ packages/@aws-cdk/aws-ecs/lib/index.ts | 7 +- 4 files changed, 104 insertions(+), 58 deletions(-) rename packages/@aws-cdk/aws-ecs/lib/{service.ts => base/base-service.ts} (62%) create mode 100644 packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts similarity index 62% rename from packages/@aws-cdk/aws-ecs/lib/service.ts rename to packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index d03876bf485a5..7e50462ae510b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -1,23 +1,11 @@ import ec2 = require('@aws-cdk/aws-ec2'); -import elb = require('@aws-cdk/aws-elasticloadbalancing'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { Cluster } from './cluster'; -import { cloudformation } from './ecs.generated'; -import { TaskDefinition } from './task-definition'; - -export interface ServiceProps { - /** - * Cluster where service will be deployed - */ - cluster: Cluster; // should be required? do we assume 'default' exists? - - /** - * Task Definition used for running tasks in the service - */ - taskDefinition: TaskDefinition; +import { BaseTaskDefinition } from '../base/base-task-definition'; +import { cloudformation } from '../ecs.generated'; +export interface BaseServiceProps { /** * Number of desired copies of running tasks * @@ -32,13 +20,6 @@ export interface ServiceProps { */ serviceName?: string; - /** - * Whether the service is hosted in EC2 or Fargate - * - * @default EC2 - */ - launchType?: LaunchType; - /** * The maximum number of tasks, specified as a percentage of the Amazon ECS * service's DesiredCount value, that can run in a service during a @@ -83,16 +64,15 @@ export interface ServiceProps { //////////////////////////////////////////// } -export class Service extends cdk.Construct implements elb.ILoadBalancerTarget, - elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { +export abstract class BaseService extends cdk.Construct implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { public readonly dependencyElements: cdk.IDependable[]; public readonly connections: ec2.Connections; - private loadBalancers = new Array(); - private readonly taskDefinition: TaskDefinition; + protected loadBalancers = new Array(); + protected readonly abstract taskDef: BaseTaskDefinition; private role?: iam.Role; private readonly resource: cloudformation.ServiceResource; - constructor(parent: cdk.Construct, name: string, props: ServiceProps) { + constructor(parent: cdk.Construct, name: string, props: BaseServiceProps, additionalProps: any) { super(parent, name); this.connections = new ec2.Connections({ @@ -104,47 +84,31 @@ export class Service extends cdk.Construct implements elb.ILoadBalancerTarget, }, }); - this.taskDefinition = props.taskDefinition; - if (!this.taskDefinition.defaultContainer) { - throw new Error('A TaskDefinition must have at least one essential container'); - } + // this.taskDefinition = props.taskDefinition; this.role = props.role; this.resource = new cloudformation.ServiceResource(this, "Service", { - cluster: props.cluster.clusterName, - taskDefinition: props.taskDefinition.taskDefinitionArn, desiredCount: props.desiredCount, serviceName: props.serviceName, - launchType: props.launchType, loadBalancers: new cdk.Token(() => this.loadBalancers), deploymentConfiguration: { maximumPercent: props.maximumPercent, minimumHealthyPercent: props.minimumHealthyPercent }, role: new cdk.Token(() => this.role && this.role.roleArn), + ...additionalProps }); this.dependencyElements = [this.resource]; } - public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { - // FIXME: If Fargate then throw a helpful error - // FIXME: Security Groups - this.loadBalancers.push({ - loadBalancerName: loadBalancer.loadBalancerName, - containerName: this.taskDefinition.defaultContainer!.name, - containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(true), - }); - this.createLoadBalancerRole(); - } - public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { // FIXME: Security Groups this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, - containerName: this.taskDefinition.defaultContainer!.name, - containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(false), + containerName: this.taskDef.defaultContainer!.name, + containerPort: this.taskDef.defaultContainer!.loadBalancerPort(false), }); this.createLoadBalancerRole(); @@ -154,15 +118,15 @@ export class Service extends cdk.Construct implements elb.ILoadBalancerTarget, public attachToNetworkTargetGroup(targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, - containerName: this.taskDefinition.defaultContainer!.name, - containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(false), + containerName: this.taskDef.defaultContainer!.name, + containerPort: this.taskDef.defaultContainer!.loadBalancerPort(false), }); this.createLoadBalancerRole(); return { targetType: elbv2.TargetType.SelfRegistering }; } - public createLoadBalancerRole() { + protected createLoadBalancerRole() { if (!this.role) { this.role = new iam.Role(this, 'Role', { assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), @@ -171,10 +135,4 @@ export class Service extends cdk.Construct implements elb.ILoadBalancerTarget, this.resource.addDependency(this.role); } } - } - -export enum LaunchType { - EC2 = 'EC2', - Fargate = 'FARGATE' -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts new file mode 100644 index 0000000000000..682a5290dd6af --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -0,0 +1,48 @@ +import elb = require('@aws-cdk/aws-elasticloadbalancing'); +import cdk = require('@aws-cdk/cdk'); +import { BaseService, BaseServiceProps } from '../base/base-service'; +import { BaseTaskDefinition } from '../base/base-task-definition'; +import { EcsCluster } from './ecs-cluster'; +import { EcsTaskDefinition } from './ecs-task-definition'; + +export interface EcsServiceProps extends BaseServiceProps { + /** + * Cluster where service will be deployed + */ + cluster: EcsCluster; // should be required? do we assume 'default' exists? + + /** + * Task Definition used for running tasks in the service + */ + taskDefinition: EcsTaskDefinition; +} + +export class EcsService extends BaseService implements elb.ILoadBalancerTarget { + protected readonly taskDef: BaseTaskDefinition; + private readonly taskDefinition: EcsTaskDefinition; + + constructor(parent: cdk.Construct, name: string, props: EcsServiceProps) { + super(parent, name, props, { + cluster: props.cluster.clusterName, + taskDefinition: props.taskDefinition.taskDefinitionArn, + launchType: 'EC2' + }); + + if (!this.taskDefinition.defaultContainer) { + throw new Error('A TaskDefinition must have at least one essential container'); + } + + this.taskDefinition = props.taskDefinition; + this.taskDef = props.taskDefinition; + } + + public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { + // FIXME: Security Groups + this.loadBalancers.push({ + loadBalancerName: loadBalancer.loadBalancerName, + containerName: this.taskDefinition.defaultContainer!.name, + containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(true), + }); + this.createLoadBalancerRole(); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts new file mode 100644 index 0000000000000..dab41cc3c70df --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -0,0 +1,37 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseService, BaseServiceProps } from '../base/base-service'; +import { BaseTaskDefinition } from '../base/base-task-definition'; +import { FargateCluster } from './fargate-cluster'; +import { FargateTaskDefinition } from './fargate-task-definition'; + +export interface FargateServiceProps extends BaseServiceProps { + /** + * Cluster where service will be deployed + */ + cluster: FargateCluster; // should be required? do we assume 'default' exists? + + /** + * Task Definition used for running tasks in the service + */ + taskDefinition: FargateTaskDefinition; +} + +export class FargateService extends BaseService { + protected readonly taskDef: BaseTaskDefinition; + private readonly taskDefinition: FargateTaskDefinition; + + constructor(parent: cdk.Construct, name: string, props: FargateServiceProps) { + super(parent, name, props, { + cluster: props.cluster.clusterName, + taskDefinition: props.taskDefinition.taskDefinitionArn, + launchType: 'FARGATE' + }); + + if (!this.taskDefinition.defaultContainer) { + throw new Error('A TaskDefinition must have at least one essential container'); + } + + this.taskDefinition = props.taskDefinition; + this.taskDef = props.taskDefinition; + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index ec49b26c2e0b9..bc7efd9f0b8b8 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,7 +1,10 @@ export * from './base/base-cluster'; export * from './ecs/ecs-cluster'; -// export * from './fargate/fargate-cluster'; -export * from './service'; +export * from './fargate/fargate-cluster'; + +export * from './base/base-service'; +export * from './ecs/ecs-service'; +export * from './fargate/fargate-service'; export * from './task-definition'; export * from './container-definition'; export * from './container-image'; From b8cce3aeb65ba3a2c41a236181b68e362d291498 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 18:59:41 +0200 Subject: [PATCH 028/140] Separate ECS/Fargate TaskDefinition --- .../base-task-definition.ts} | 113 ++---------------- .../aws-ecs/lib/ecs/ecs-task-definition.ts | 81 +++++++++++++ .../lib/fargate/fargate-task-definition.ts | 48 ++++++++ packages/@aws-cdk/aws-ecs/lib/index.ts | 5 +- 4 files changed, 143 insertions(+), 104 deletions(-) rename packages/@aws-cdk/aws-ecs/lib/{task-definition.ts => base/base-task-definition.ts} (51%) create mode 100644 packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts similarity index 51% rename from packages/@aws-cdk/aws-ecs/lib/task-definition.ts rename to packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index d41d74679f5de..46b99085c8b15 100644 --- a/packages/@aws-cdk/aws-ecs/lib/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -1,26 +1,9 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { ContainerDefinition } from './container-definition'; -import { cloudformation } from './ecs.generated'; - -export interface TaskDefinitionProps { - /** - * The number of cpu units used by the task. If using the EC2 launch type, - * this field is optional. Supported values are between 128 CPU units - * (0.125 vCPUs) and 10240 CPU units (10 vCPUs). If you are using the - * Fargate launch type, this field is required and you must use one of the - * following values, which determines your range of valid values for the - * memory parameter: - * 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB - * 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB - * 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - * 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments - * 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments - * - * @default 256 - */ - cpu?: string; +import { ContainerDefinition } from '../container-definition'; +import { cloudformation } from '../ecs.generated'; +export interface BaseTaskDefinitionProps { /** * Namespace for task definition versions * @@ -28,52 +11,6 @@ export interface TaskDefinitionProps { */ family?: string; - /** - * The amount (in MiB) of memory used by the task. If using the EC2 launch - * type, this field is optional and any value can be used. If you are using - * the Fargate launch type, this field is required and you must use one of - * the following values, which determines your range of valid values for - * the cpu parameter: - * - * 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU) - * - * 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU) - * - * 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU) - * - * Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU) - * - * Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU) - * - * @default 512 - */ - memoryMiB?: string; - - /** - * The Docker networking mode to use for the containers in the task. - * - * For Fargate or to use task networking, "awsvpc" mode is required. - * - * @default NetworkMode.Bridge - */ - networkMode?: NetworkMode; - - /** - * An array of placement constraint objects to use for the task. You can - * specify a maximum of 10 constraints per task (this limit includes - * constraints in the task definition and those specified at run time). - * - * Not supported in Fargate. - */ - placementConstraints?: PlacementConstraint[]; - - /** - * Valid values include EC2 and FARGATE. - * - * @default EC2 - */ - // requiresCompatibilities?: string[]; // FARGATE or EC2 -- set on ECS TD vs FG TD - /** * The IAM role assumed by the ECS agent. * @@ -97,11 +34,10 @@ export interface TaskDefinitionProps { volumes?: Volume[]; } -export class TaskDefinition extends cdk.Construct { +export class BaseTaskDefinition extends cdk.Construct { public readonly family: string; public readonly taskDefinitionArn: string; public readonly taskRole: iam.Role; - public readonly networkMode: NetworkMode; /** * Default container for this task @@ -112,19 +48,13 @@ export class TaskDefinition extends cdk.Construct { */ public defaultContainer?: ContainerDefinition; private readonly containers = new Array(); - private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; private executionRole?: iam.Role; - constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { + constructor(parent: cdk.Construct, name: string, props: BaseTaskDefinitionProps, additionalProps: any) { super(parent, name); this.family = props.family || this.uniqueId; - this.networkMode = props.networkMode || NetworkMode.Bridge; - - if (props.placementConstraints) { - props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); - } if (props.volumes) { props.volumes.forEach(v => this.addVolume(v)); @@ -138,13 +68,10 @@ export class TaskDefinition extends cdk.Construct { const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { containerDefinitions: new cdk.Token(() => this.containers.map(x => x.renderContainerDefinition())), - cpu: props.cpu, executionRoleArn: new cdk.Token(() => this.executionRole && this.executionRole.roleArn), family: this.family, - memory: props.memoryMiB, - networkMode: this.networkMode, - placementConstraints: new cdk.Token(() => this.placementConstraints), - taskRoleArn: this.taskRole.roleArn + taskRoleArn: this.taskRole.roleArn, + ...additionalProps }); this.taskDefinitionArn = taskDef.taskDefinitionArn; @@ -170,26 +97,11 @@ export class TaskDefinition extends cdk.Construct { } } - /** - * Constrain where this task can be placed - */ - private addPlacementConstraint(constraint: PlacementConstraint) { - const pc = this.renderPlacementConstraint(constraint); - this.placementConstraints.push(pc); - } - private addVolume(volume: Volume) { // const v = this.renderVolume(volume); this.volumes.push(volume); } - private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { - return { - type: pc.type, - expression: pc.expression - }; - } - /** * Generate a default execution role that allows pulling from ECR */ @@ -231,14 +143,9 @@ export enum NetworkMode { Host = 'host', } -export interface PlacementConstraint { - expression?: string; - type: PlacementConstraintType; -} - -export enum PlacementConstraintType { - DistinctInstance = "distinctInstance", - MemberOf = "memberOf" +export enum Compatibilities { + Ec2 = "EC2", + Fargate = "FARGATE" } export interface Volume { diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts new file mode 100644 index 0000000000000..ca82b837bf634 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts @@ -0,0 +1,81 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseTaskDefinition, BaseTaskDefinitionProps, Compatibilities, NetworkMode } from '../base/base-task-definition'; +import { cloudformation } from '../ecs.generated'; + +export interface EcsTaskDefinitionProps extends BaseTaskDefinitionProps { + /** + * The number of cpu units used by the task. If using the EC2 launch type, + * this field is optional. Supported values are between 128 CPU units + * (0.125 vCPUs) and 10240 CPU units (10 vCPUs). + * + * @default 256 + */ + cpu?: string; + + /** + * The amount (in MiB) of memory used by the task. If using the EC2 launch type, this field is optional and any value + * can be used. + * + * @default 512 + */ + memoryMiB?: string; + + /** + * The Docker networking mode to use for the containers in the task. + * + * @default NetworkMode.Bridge + */ + networkMode?: NetworkMode; + + /** + * An array of placement constraint objects to use for the task. You can + * specify a maximum of 10 constraints per task (this limit includes + * constraints in the task definition and those specified at run time). + * + * Not supported in Fargate. + */ + placementConstraints?: PlacementConstraint[]; +} + +export class EcsTaskDefinition extends BaseTaskDefinition { + private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; + + constructor(parent: cdk.Construct, name: string, props: EcsTaskDefinitionProps) { + super(parent, name, props, { + cpu: props.cpu, + memoryMiB: props.memoryMiB, + networkMode: props.networkMode || NetworkMode.Bridge, + requiresCompatibilities: Compatibilities.Ec2, + placementConstraints: new cdk.Token(() => this.placementConstraints) + }); + + if (props.placementConstraints) { + props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); + } + } + + /** + * Constrain where tasks can be placed + */ + private addPlacementConstraint(constraint: PlacementConstraint) { + const pc = this.renderPlacementConstraint(constraint); + this.placementConstraints.push(pc); + } + + private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { + return { + type: pc.type, + expression: pc.expression + }; + } +} + +export interface PlacementConstraint { + expression?: string; + type: PlacementConstraintType; +} + +export enum PlacementConstraintType { + DistinctInstance = "distinctInstance", + MemberOf = "memberOf" +} diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts new file mode 100644 index 0000000000000..d366c28ba2cf1 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -0,0 +1,48 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseTaskDefinition, BaseTaskDefinitionProps, Compatibilities, NetworkMode } from '../base/base-task-definition'; + +export interface FargateTaskDefinitionProps extends BaseTaskDefinitionProps { + /** + * The number of cpu units used by the task. + * Valid values, which determines your range of valid values for the memory parameter: + * 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB + * 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB + * 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB + * 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments + * 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments + * + * @default 256 + */ + cpu: string; + + /** + * The amount (in MiB) of memory used by the task. + * + * This field is required and you must use one of the following values, which determines your range of valid values + * for the cpu parameter: + * + * 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU) + * + * 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU) + * + * 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU) + * + * Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU) + * + * Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU) + * + * @default 512 + */ + memoryMiB: string; +} + +export class FargateTaskDefinition extends BaseTaskDefinition { + constructor(parent: cdk.Construct, name: string, props: FargateTaskDefinitionProps) { + super(parent, name, props, { + cpu: props.cpu, + memoryMiB: props.memoryMiB, + networkMode: NetworkMode.AwsVpc, + requiresCompatibilities: Compatibilities.Fargate + }); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index bc7efd9f0b8b8..5c3170c2fe003 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -5,7 +5,10 @@ export * from './fargate/fargate-cluster'; export * from './base/base-service'; export * from './ecs/ecs-service'; export * from './fargate/fargate-service'; -export * from './task-definition'; + +export * from './base/base-task-definition'; +export * from './ecs/ecs-task-definition'; +export * from './fargate/fargate-task-definition'; export * from './container-definition'; export * from './container-image'; export * from './linux-parameters'; From 274160811aa4ebb73dd3ff81697ba1e38eda5cc6 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 19:00:05 +0200 Subject: [PATCH 029/140] Fix lint errors --- packages/@aws-cdk/aws-ecs/lib/container-definition.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 98a9a50ec63a0..95080b18a446d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -317,7 +317,7 @@ function renderHealthCheck(hc: HealthCheck): cloudformation.TaskDefinitionResour } function getHealthCheckCommand(hc: HealthCheck): string[] { - let cmd = hc.command; + const cmd = hc.command; const hcCommand = new Array(); if (cmd.length === 0) { @@ -330,7 +330,7 @@ function getHealthCheckCommand(hc: HealthCheck): string[] { } if (cmd[0] !== "CMD" || cmd[0] !== 'CMD-SHELL') { - hcCommand.push('CMD') + hcCommand.push('CMD'); } return hcCommand.concat(cmd); From a901912f4bbf7961d98a84d0fef405aa486ca8c8 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 1 Oct 2018 19:00:05 +0200 Subject: [PATCH 030/140] Fix lint errors --- packages/@aws-cdk/aws-ecs/lib/base/base-service.ts | 3 ++- packages/@aws-cdk/aws-ecs/lib/container-definition.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 7e50462ae510b..cb9be35c47ce8 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -64,7 +64,8 @@ export interface BaseServiceProps { //////////////////////////////////////////// } -export abstract class BaseService extends cdk.Construct implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { +export abstract class BaseService extends cdk.Construct + implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { public readonly dependencyElements: cdk.IDependable[]; public readonly connections: ec2.Connections; protected loadBalancers = new Array(); diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 98a9a50ec63a0..95080b18a446d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -317,7 +317,7 @@ function renderHealthCheck(hc: HealthCheck): cloudformation.TaskDefinitionResour } function getHealthCheckCommand(hc: HealthCheck): string[] { - let cmd = hc.command; + const cmd = hc.command; const hcCommand = new Array(); if (cmd.length === 0) { @@ -330,7 +330,7 @@ function getHealthCheckCommand(hc: HealthCheck): string[] { } if (cmd[0] !== "CMD" || cmd[0] !== 'CMD-SHELL') { - hcCommand.push('CMD') + hcCommand.push('CMD'); } return hcCommand.concat(cmd); From 51adff56276ff003d6d908ac7b8e310670ddce4e Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 11:09:59 +0200 Subject: [PATCH 031/140] Fix bugs in service and task def --- packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 6 +++--- packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts | 2 +- .../@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 682a5290dd6af..cd3ff5b7be29f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -28,12 +28,12 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { launchType: 'EC2' }); + this.taskDefinition = props.taskDefinition; + this.taskDef = props.taskDefinition; + if (!this.taskDefinition.defaultContainer) { throw new Error('A TaskDefinition must have at least one essential container'); } - - this.taskDefinition = props.taskDefinition; - this.taskDef = props.taskDefinition; } public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts index ca82b837bf634..02d9f3de2b894 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts @@ -45,7 +45,7 @@ export class EcsTaskDefinition extends BaseTaskDefinition { cpu: props.cpu, memoryMiB: props.memoryMiB, networkMode: props.networkMode || NetworkMode.Bridge, - requiresCompatibilities: Compatibilities.Ec2, + requiresCompatibilities: [Compatibilities.Ec2], placementConstraints: new cdk.Token(() => this.placementConstraints) }); diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index d366c28ba2cf1..a1ac36c29b47c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -42,7 +42,7 @@ export class FargateTaskDefinition extends BaseTaskDefinition { cpu: props.cpu, memoryMiB: props.memoryMiB, networkMode: NetworkMode.AwsVpc, - requiresCompatibilities: Compatibilities.Fargate + requiresCompatibilities: [Compatibilities.Fargate] }); } } From 70ae6827879bc8abbf2695281e914d8a64fd5eb9 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 11:26:10 +0200 Subject: [PATCH 032/140] Fix AMI ID getter for ECS clusters --- .gitignore | 1 + packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 5a03c2b774932..7d57ce2c8dfe0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ coverage .nyc_output .LAST_BUILD *.swp +examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index a4231f12b07e1..a35a54c29ccb2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -86,12 +86,14 @@ export class EcsCluster extends BaseCluster { * Construct a Linux machine image from the latest ECS Optimized AMI published in SSM */ export class EcsOptimizedAmi implements ec2.IMachineImageSource { - private static AmiParamterName = "/aws/service/ecs/optimized-ami/amazon-linux/recommended"; + private static AmiParameterName = "/aws/service/ecs/optimized-ami/amazon-linux/recommended"; public getImage(parent: cdk.Construct): ec2.MachineImage { const ssmProvider = new cdk.SSMParameterProvider(parent); - const ami = ssmProvider.getString(EcsOptimizedAmi.AmiParamterName); + const json = ssmProvider.getString(EcsOptimizedAmi.AmiParameterName); + const ami = JSON.parse(json).image_id; + return new ec2.MachineImage(ami, new ec2.LinuxOS()); } } From 298e0a796daef9f61588bf718b7ab6f69e7b91bc Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 11:26:29 +0200 Subject: [PATCH 033/140] Add Capabilities in Linux Parameters on ContainerDefs --- .../hello-cdk-ecs/index.ts | 99 ++++++++++--------- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 80 +++++++++++++-- 2 files changed, 128 insertions(+), 51 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 3a2a0340b56e5..d520648fbc19f 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -3,51 +3,60 @@ import ecs = require('@aws-cdk/aws-ecs'); import cdk = require('@aws-cdk/cdk'); class BonjourECS extends cdk.Stack { - constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { - super(parent, name, props); - - // For better iteration speed, it might make sense to put this VPC into - // a separate stack and import it here. We then have two stacks to - // deploy, but VPC creation is slow so we'll only have to do that once - // and can iterate quickly on consuming stacks. Not doing that for now. - const vpc = new ec2.VpcNetwork(this, 'MyVpc', { - maxAZs: 2 - }); - - const cluster = new ecs.Cluster(this, 'DemoCluster', { - vpc - }); - - // name, image, cpu, memory, port (with default) - // - // Include in constructs: - // - networking - include SD, ALB - // - logging - cloudwatch logs integration? talk to nathan about 3rd - // party integrations - aggregated logging across the service - // (instead of per task). Probably prometheus or elk? - // - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, - // zipkin. - // - so x-ray is a container that is hooked up to sidecars that come - // with the application container itself - // - autoscaling - application autoscaling (Fargate focused?) - - const taskDefinition = new ecs.TaskDefinition(this, "MyTD", { - family: "ecs-task-definition", - placementConstraints: [{ - type: ecs.PlacementConstraintType.DistinctInstance - }], - }); - - taskDefinition.addContainer(new ecs.ContainerDefinition(this, 'Def', { - name: "web", - image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - cpu: 1024, - memoryMiB: 512, - essential: true - })); - - cluster.runService(taskDefinition); - } + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + + // For better iteration speed, it might make sense to put this VPC into + // a separate stack and import it here. We then have two stacks to + // deploy, but VPC creation is slow so we'll only have to do that once + // and can iterate quickly on consuming stacks. Not doing that for now. + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { + maxAZs: 2 + }); + + const cluster = new ecs.EcsCluster(this, 'EcsCluster', { + vpc + }); + + // name, image, cpu, memory, port (with default) + // + // Include in constructs: + // - networking - include SD, ALB + // - logging - cloudwatch logs integration? talk to nathan about 3rd + // party integrations - aggregated logging across the service + // (instead of per task). Probably prometheus or elk? + // - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, + // zipkin. + // - so x-ray is a container that is hooked up to sidecars that come + // with the application container itself + // - autoscaling - application autoscaling (Fargate focused?) + + const taskDefinition = new ecs.EcsTaskDefinition(this, "EcsTD", { + family: "ecs-task-definition", + placementConstraints: [{ + type: ecs.PlacementConstraintType.DistinctInstance + }], + }); + + const container = new ecs.ContainerDefinition(this, 'Container', { + name: "web", + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + cpu: 1024, + memoryMiB: 512, + essential: true + }); + + container.linuxParameters.addCapability(ecs.Capability.All); + + taskDefinition.addContainer(container); + + new ecs.EcsService(this, "EcsService", { + cluster, + taskDefinition, + desiredCount: 1, + }); + // cluster.runService(taskDefinition); + } } const app = new cdk.App(process.argv); diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index b9ea35bb61317..1ef970fcf3e19 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -1,16 +1,84 @@ import { cloudformation } from './ecs.generated'; export class LinuxParameters { + public initProcessEnabled?: boolean; - public addCapability() { - // FIXME + public sharedMemorySize?: number; + + private readonly addCapabilities: Capability[] = []; + + private readonly dropCapabilities: Capability[] = []; + + // private readonly devices: Device[] = []; + + // private readonly tmpfs: Tmpfs[] = []; + + /** + * Only works with EC2 launch type + */ + public addCapability(...cap: Capability[]) { + this.addCapabilities.push(...cap); } - public dropCapability() { - // FIXME + public dropCapability(...cap: Capability[]) { + this.dropCapabilities.push(...cap); } public toLinuxParametersJson(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { - return {}; + return { + initProcessEnabled: this.initProcessEnabled, + sharedMemorySize: this.sharedMemorySize, + capabilities: { + add: this.addCapabilities, + drop: this.dropCapabilities, + } + }; } -} \ No newline at end of file +} + +// export interface Device { +// } + +// export interface Tmpfs { +// } + +export enum Capability { + All = "ALL", + AuditControl = "AUDIT_CONTROL", + AuditWrite = "AUDIT_WRITE", + BlockSuspend = "BLOCK_SUSPEND", + Chown = "CHOWN", + DacOverride = "DAC_OVERRIDE", + DacReadSearch = "DAC_READ_SEARCH", + Fowner = "FOWNER", + Fsetid = "FSETID", + IpcLock = "IPC_LOCK", + IpcOwner = "IPC_OWNER", + Kill = "KILL", + Lease = "LEASE", + LinuxImmutable = "LINUX_IMMUTABLE", + MacAdmin = "MAC_ADMIN", + MacOverride = "MAC_OVERRIDE", + Mknod = "MKNOD", + NetAdmin = "NET_ADMIN", + NetBindService = "NET_BIND_SERVICE", + NetBroadcast = "NET_BROADCAST", + NetRaw = "NET_RAW", + Setfcap = "SETFCAP", + Setgid = "SETGID", + Setpcap = "SETPCAP", + Setuid = "SETUID", + SysAdmin = "SYS_ADMIN", + SysBoot = "SYS_BOOT", + SysChroot = "SYS_CHROOT", + SysModule = "SYS_MODULE", + SysNice = "SYS_NICE", + SysPacct = "SYS_PACCT", + SysPtrace = "SYS_PTRACE", + SysRawio = "SYS_RAWIO", + SysResource = "SYS_RESOURCE", + SysTime = "SYS_TIME", + SysTtyConfig = "SYS_TTY_CONFIG", + Syslog = "SYSLOG", + WakeAlarm = "WAKE_ALARM" +} From 1541edd645e65c2f5fdbf7cd9b790a427027e571 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 11:26:29 +0200 Subject: [PATCH 034/140] Add Capabilities in Linux Parameters on ContainerDefs --- .../hello-cdk-ecs/index.ts | 99 ++++++++++--------- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 80 +++++++++++++-- 2 files changed, 128 insertions(+), 51 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 3a2a0340b56e5..d520648fbc19f 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -3,51 +3,60 @@ import ecs = require('@aws-cdk/aws-ecs'); import cdk = require('@aws-cdk/cdk'); class BonjourECS extends cdk.Stack { - constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { - super(parent, name, props); - - // For better iteration speed, it might make sense to put this VPC into - // a separate stack and import it here. We then have two stacks to - // deploy, but VPC creation is slow so we'll only have to do that once - // and can iterate quickly on consuming stacks. Not doing that for now. - const vpc = new ec2.VpcNetwork(this, 'MyVpc', { - maxAZs: 2 - }); - - const cluster = new ecs.Cluster(this, 'DemoCluster', { - vpc - }); - - // name, image, cpu, memory, port (with default) - // - // Include in constructs: - // - networking - include SD, ALB - // - logging - cloudwatch logs integration? talk to nathan about 3rd - // party integrations - aggregated logging across the service - // (instead of per task). Probably prometheus or elk? - // - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, - // zipkin. - // - so x-ray is a container that is hooked up to sidecars that come - // with the application container itself - // - autoscaling - application autoscaling (Fargate focused?) - - const taskDefinition = new ecs.TaskDefinition(this, "MyTD", { - family: "ecs-task-definition", - placementConstraints: [{ - type: ecs.PlacementConstraintType.DistinctInstance - }], - }); - - taskDefinition.addContainer(new ecs.ContainerDefinition(this, 'Def', { - name: "web", - image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - cpu: 1024, - memoryMiB: 512, - essential: true - })); - - cluster.runService(taskDefinition); - } + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + + // For better iteration speed, it might make sense to put this VPC into + // a separate stack and import it here. We then have two stacks to + // deploy, but VPC creation is slow so we'll only have to do that once + // and can iterate quickly on consuming stacks. Not doing that for now. + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { + maxAZs: 2 + }); + + const cluster = new ecs.EcsCluster(this, 'EcsCluster', { + vpc + }); + + // name, image, cpu, memory, port (with default) + // + // Include in constructs: + // - networking - include SD, ALB + // - logging - cloudwatch logs integration? talk to nathan about 3rd + // party integrations - aggregated logging across the service + // (instead of per task). Probably prometheus or elk? + // - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, + // zipkin. + // - so x-ray is a container that is hooked up to sidecars that come + // with the application container itself + // - autoscaling - application autoscaling (Fargate focused?) + + const taskDefinition = new ecs.EcsTaskDefinition(this, "EcsTD", { + family: "ecs-task-definition", + placementConstraints: [{ + type: ecs.PlacementConstraintType.DistinctInstance + }], + }); + + const container = new ecs.ContainerDefinition(this, 'Container', { + name: "web", + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + cpu: 1024, + memoryMiB: 512, + essential: true + }); + + container.linuxParameters.addCapability(ecs.Capability.All); + + taskDefinition.addContainer(container); + + new ecs.EcsService(this, "EcsService", { + cluster, + taskDefinition, + desiredCount: 1, + }); + // cluster.runService(taskDefinition); + } } const app = new cdk.App(process.argv); diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index b9ea35bb61317..1be5327c704ab 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -1,16 +1,84 @@ import { cloudformation } from './ecs.generated'; export class LinuxParameters { + public initProcessEnabled?: boolean; - public addCapability() { - // FIXME + public sharedMemorySize?: number; + + private readonly addCapabilities: Capability[] = []; + + private readonly dropCapabilities: Capability[] = []; + + // private readonly devices: Device[] = []; + + // private readonly tmpfs: Tmpfs[] = []; + + /** + * AddCapability only works with EC2 launch type + */ + public addCapability(...cap: Capability[]) { + this.addCapabilities.push(...cap); } - public dropCapability() { - // FIXME + public dropCapability(...cap: Capability[]) { + this.dropCapabilities.push(...cap); } public toLinuxParametersJson(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { - return {}; + return { + initProcessEnabled: this.initProcessEnabled, + sharedMemorySize: this.sharedMemorySize, + capabilities: { + add: this.addCapabilities, + drop: this.dropCapabilities, + } + }; } -} \ No newline at end of file +} + +// export interface Device { +// } + +// export interface Tmpfs { +// } + +export enum Capability { + All = "ALL", + AuditControl = "AUDIT_CONTROL", + AuditWrite = "AUDIT_WRITE", + BlockSuspend = "BLOCK_SUSPEND", + Chown = "CHOWN", + DacOverride = "DAC_OVERRIDE", + DacReadSearch = "DAC_READ_SEARCH", + Fowner = "FOWNER", + Fsetid = "FSETID", + IpcLock = "IPC_LOCK", + IpcOwner = "IPC_OWNER", + Kill = "KILL", + Lease = "LEASE", + LinuxImmutable = "LINUX_IMMUTABLE", + MacAdmin = "MAC_ADMIN", + MacOverride = "MAC_OVERRIDE", + Mknod = "MKNOD", + NetAdmin = "NET_ADMIN", + NetBindService = "NET_BIND_SERVICE", + NetBroadcast = "NET_BROADCAST", + NetRaw = "NET_RAW", + Setfcap = "SETFCAP", + Setgid = "SETGID", + Setpcap = "SETPCAP", + Setuid = "SETUID", + SysAdmin = "SYS_ADMIN", + SysBoot = "SYS_BOOT", + SysChroot = "SYS_CHROOT", + SysModule = "SYS_MODULE", + SysNice = "SYS_NICE", + SysPacct = "SYS_PACCT", + SysPtrace = "SYS_PTRACE", + SysRawio = "SYS_RAWIO", + SysResource = "SYS_RESOURCE", + SysTime = "SYS_TIME", + SysTtyConfig = "SYS_TTY_CONFIG", + Syslog = "SYSLOG", + WakeAlarm = "WAKE_ALARM" +} From a154d0935eeb2bd37aebea5890f23decee390a50 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 11:40:53 +0200 Subject: [PATCH 035/140] Add default arg to SSMProvider.getString --- .gitignore | 2 +- examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json | 3 ++- packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 2 +- packages/@aws-cdk/cdk/lib/context.ts | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 7d57ce2c8dfe0..bd75b435a1ac4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ coverage .nyc_output .LAST_BUILD *.swp -examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json +./examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json index 841c948f0b918..eb5f700d36513 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json @@ -36,6 +36,7 @@ "eu-west-1a", "eu-west-1b", "eu-west-1c" - ] + ], + "ssm:794715269151:us-west-2:/aws/service/ecs/optimized-ami/amazon-linux/recommended": "{\"schema_version\":1,\"image_name\":\"amzn-ami-2018.03.g-amazon-ecs-optimized\",\"image_id\":\"ami-00430184c7bb49914\",\"os\":\"Amazon Linux\",\"ecs_runtime_version\":\"Docker version 18.06.1-ce\",\"ecs_agent_version\":\"1.20.3\"}" } } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index a35a54c29ccb2..20928345a8f00 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -91,7 +91,7 @@ export class EcsOptimizedAmi implements ec2.IMachineImageSource { public getImage(parent: cdk.Construct): ec2.MachineImage { const ssmProvider = new cdk.SSMParameterProvider(parent); - const json = ssmProvider.getString(EcsOptimizedAmi.AmiParameterName); + const json = ssmProvider.getString(EcsOptimizedAmi.AmiParameterName, "{\"image_id\": \"\"}"); const ami = JSON.parse(json).image_id; return new ec2.MachineImage(ami, new ec2.LinuxOS()); diff --git a/packages/@aws-cdk/cdk/lib/context.ts b/packages/@aws-cdk/cdk/lib/context.ts index 3c17890b6e129..e63e6aa58d201 100644 --- a/packages/@aws-cdk/cdk/lib/context.ts +++ b/packages/@aws-cdk/cdk/lib/context.ts @@ -151,9 +151,9 @@ export class SSMParameterProvider { /** * Return the SSM parameter string with the indicated key */ - public getString(parameterName: string): any { + public getString(parameterName: string, defaultValue: string = "dummy"): any { const scope = this.provider.accountRegionScope('SSMParameterProvider'); - return this.provider.getStringValue(SSM_PARAMETER_PROVIDER, scope, [parameterName], 'dummy'); + return this.provider.getStringValue(SSM_PARAMETER_PROVIDER, scope, [parameterName], defaultValue); } } From 4b466853ad3703334e3effa71a96c8f895d13a50 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 14:00:35 +0200 Subject: [PATCH 036/140] Add Devices to LinuxParameters --- .../hello-cdk-ecs/index.ts | 15 +++++++--- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 30 ++++++++++++++++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index d520648fbc19f..e1095d97a30e3 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -33,9 +33,10 @@ class BonjourECS extends cdk.Stack { const taskDefinition = new ecs.EcsTaskDefinition(this, "EcsTD", { family: "ecs-task-definition", - placementConstraints: [{ - type: ecs.PlacementConstraintType.DistinctInstance - }], + // placementConstraints: [{ + // type: ecs.PlacementConstraintType.MemberOf, + // expression: "attribute:ecs.instance-type =~ t2.*" + // }], }); const container = new ecs.ContainerDefinition(this, 'Container', { @@ -48,6 +49,12 @@ class BonjourECS extends cdk.Stack { container.linuxParameters.addCapability(ecs.Capability.All); + container.linuxParameters.addDevice({ + containerPath: "/pudding", + hostPath: "/dev/sda", + permissions: [ecs.DevicePermission.Read] + }); + taskDefinition.addContainer(container); new ecs.EcsService(this, "EcsService", { @@ -61,6 +68,6 @@ class BonjourECS extends cdk.Stack { const app = new cdk.App(process.argv); -new BonjourECS(app, 'GoedeMorgen'); +new BonjourECS(app, 'Bonjour'); process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index 1be5327c704ab..b973e646182e1 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -9,7 +9,7 @@ export class LinuxParameters { private readonly dropCapabilities: Capability[] = []; - // private readonly devices: Device[] = []; + private readonly devices: Device[] = []; // private readonly tmpfs: Tmpfs[] = []; @@ -24,6 +24,10 @@ export class LinuxParameters { this.dropCapabilities.push(...cap); } + public addDevice(...device: Device[]) { + this.devices.push(...device); + } + public toLinuxParametersJson(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { return { initProcessEnabled: this.initProcessEnabled, @@ -31,13 +35,25 @@ export class LinuxParameters { capabilities: { add: this.addCapabilities, drop: this.dropCapabilities, - } + }, + devices: this.devices.map(renderDevice) }; } } -// export interface Device { -// } +export interface Device { + containerPath?: string, + hostPath: string, + permissions?: DevicePermission[] +} + +function renderDevice(device: Device): cloudformation.TaskDefinitionResource.DeviceProperty { + return { + containerPath: device.containerPath, + hostPath: device.hostPath, + permissions: device.permissions + } +} // export interface Tmpfs { // } @@ -82,3 +98,9 @@ export enum Capability { Syslog = "SYSLOG", WakeAlarm = "WAKE_ALARM" } + +export enum DevicePermission { + Read = "read", + Write = "write", + Mknod = "mknod", +} From 9875c6e5b7d0aac370f446ca4d34849320fca737 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 14:20:50 +0200 Subject: [PATCH 037/140] Add Tmpfs to LinuxParameters --- .../hello-cdk-ecs/index.ts | 6 ++ .../aws-ecs/lib/container-definition.ts | 2 +- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 68 +++++++++++++++++-- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index e1095d97a30e3..2a054531fee2d 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -55,6 +55,12 @@ class BonjourECS extends cdk.Stack { permissions: [ecs.DevicePermission.Read] }); + container.linuxParameters.addTmpfs({ + containerPath: "/pudding", + size: 12345, + mountOptions: [ecs.TmpfsMountOption.Ro] + }); + taskDefinition.addContainer(container); new ecs.EcsService(this, "EcsService", { diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 95080b18a446d..f3827fc5be7d8 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -240,7 +240,7 @@ export class ContainerDefinition extends cdk.Construct { extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'), healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), links: this.links, - linuxParameters: this.linuxParameters.toLinuxParametersJson(), + linuxParameters: this.linuxParameters.renderLinuxParameters(), }; } } diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index b973e646182e1..21a149310b65b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -11,7 +11,7 @@ export class LinuxParameters { private readonly devices: Device[] = []; - // private readonly tmpfs: Tmpfs[] = []; + private readonly tmpfs: Tmpfs[] = []; /** * AddCapability only works with EC2 launch type @@ -28,7 +28,11 @@ export class LinuxParameters { this.devices.push(...device); } - public toLinuxParametersJson(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { + public addTmpfs(...tmpfs: Tmpfs[]) { + this.tmpfs.push(...tmpfs); + } + + public renderLinuxParameters(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { return { initProcessEnabled: this.initProcessEnabled, sharedMemorySize: this.sharedMemorySize, @@ -36,7 +40,8 @@ export class LinuxParameters { add: this.addCapabilities, drop: this.dropCapabilities, }, - devices: this.devices.map(renderDevice) + devices: this.devices.map(renderDevice), + tmpfs: this.tmpfs.map(renderTmpfs) }; } } @@ -55,8 +60,19 @@ function renderDevice(device: Device): cloudformation.TaskDefinitionResource.Dev } } -// export interface Tmpfs { -// } +export interface Tmpfs { + containerPath: string, + size: number, + mountOptions?: TmpfsMountOption[], +} + +function renderTmpfs(tmpfs: Tmpfs): cloudformation.TaskDefinitionResource.TmpfsProperty { + return { + containerPath: tmpfs.containerPath, + size: tmpfs.size, + mountOptions: tmpfs.mountOptions + } +} export enum Capability { All = "ALL", @@ -104,3 +120,45 @@ export enum DevicePermission { Write = "write", Mknod = "mknod", } + +export enum TmpfsMountOption { + Defaults = "defaults", + Ro = "ro", + Rw = "rw", + Suid = "suid", + Nosuid = "nosuid", + Dev = "dev", + Nodev = "nodev", + Exec = "exec", + Noexec = "noexec", + Sync = "sync", + Async = "async", + Dirsync = "dirsync", + Remount = "remount", + Mand = "mand", + Nomand = "nomand", + Atime = "atime", + Noatime = "noatime", + Diratime = "diratime", + Nodiratime = "nodiratime", + Bind = "bind", + Rbind = "rbind", + Unbindable = "unbindable", + Runbindable = "runbindable", + Private = "private", + Rprivate = "rprivate", + Shared = "shared", + Rshared = "rshared", + Slave = "slave", + Rslave = "rslave", + Relatime = "relatime", + Norelatime = "norelatime", + Strictatime = "strictatime", + Nostrictatime = "nostrictatime", + Mode = "mode", + Uid = "uid", + Gid = "gid", + NrInodes = "nr_inodes", + NrBlocks = "nr_blocks", + Mpol = "mpol" +} From 1044d1c4e3a3564008fcfc0ac80e34b78735145e Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 14:26:05 +0200 Subject: [PATCH 038/140] Update ecs demo with full Linux Parameters example --- examples/cdk-examples-typescript/hello-cdk-ecs/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 2a054531fee2d..ef757482685b1 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -61,6 +61,9 @@ class BonjourECS extends cdk.Stack { mountOptions: [ecs.TmpfsMountOption.Ro] }); + container.linuxParameters.sharedMemorySize = 65535 + container.linuxParameters.initProcessEnabled = true + taskDefinition.addContainer(container); new ecs.EcsService(this, "EcsService", { From e7f8000706e558d57408d9e0237694f4b4c31295 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 14:50:30 +0200 Subject: [PATCH 039/140] Rename renderLogDriver --- packages/@aws-cdk/aws-ecs/lib/container-definition.ts | 3 +-- packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index f3827fc5be7d8..f6ab4dfe0c576 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -235,7 +235,7 @@ export class ContainerDefinition extends cdk.Construct { user: this.props.user, volumesFrom: [], // FIXME workingDirectory: this.props.workingDirectory, - logConfiguration: this.props.logging && this.props.logging.toLogDriverJson(), + logConfiguration: this.props.logging && this.props.logging.renderLogDriver(), environment: this.props.environment && renderKV(this.props.environment, 'name', 'value'), extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'), healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), @@ -295,7 +295,6 @@ export interface HealthCheck { // mountPoints?: mountPoint[]; // portMappings?: portMapping[]; -// ulimits?: ulimit[]; // volumesFrom?: volumeFrom[]; function renderKV(env: {[key: string]: string}, keyName: string, valueName: string): any { diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts index 65b34e4b51737..8e8caa8f5e8e1 100644 --- a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts @@ -64,7 +64,7 @@ export class AwsLogDriver extends LogDriver { /** * Return the log driver CloudFormation JSON */ - public toLogDriverJson(): cloudformation.TaskDefinitionResource.LogConfigurationProperty { + public renderLogDriver(): cloudformation.TaskDefinitionResource.LogConfigurationProperty { return { logDriver: 'awslogs', options: removeEmpty({ @@ -88,4 +88,4 @@ function removeEmpty(x: {[key: string]: (T | undefined)}): {[key: string]: T} } } return x as any; -} \ No newline at end of file +} From e7f7b7d824c5289be4a24d6ceeb16ebeb2e6eaf8 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 2 Oct 2018 14:54:08 +0200 Subject: [PATCH 040/140] Do most of Service --- .../@aws-cdk/aws-ecs/lib/base/base-cluster.ts | 5 + .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 94 ++++++--- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 188 +++++++++++++++++- .../aws-ecs/lib/ecs/ecs-task-definition.ts | 10 +- .../aws-ecs/lib/fargate/fargate-service.ts | 32 ++- 5 files changed, 298 insertions(+), 31 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts index f416cf46351dd..b9944cf4e1344 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts @@ -17,6 +17,10 @@ export interface BaseClusterProps { } export class BaseCluster extends cdk.Construct { + /** + * The VPC this cluster was created in + */ + public readonly vpc: ec2.VpcNetworkRef; public readonly clusterArn: string; @@ -27,6 +31,7 @@ export class BaseCluster extends cdk.Construct { const cluster = new cloudformation.ClusterResource(this, 'Resource', {clusterName: props.clusterName}); + this.vpc = props.vpc; this.clusterArn = cluster.clusterArn; this.clusterName = cluster.ref; } diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index cb9be35c47ce8..1205f66642164 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -52,41 +52,31 @@ export interface BaseServiceProps { */ healthCheckGracePeriodSeconds?: number; - ///////// TBD /////////////////////////////// - // healthCheckGracePeriodSeconds?: number; // only needed with load balancers - // loadBalancers?: LoadBalancer[]; - // placementConstraints?: PlacementConstraint[]; - // placementStrategies?: PlacementStrategy[]; - // networkConfiguration?: NetworkConfiguration; - // serviceRegistries?: ServiceRegistry[]; - // - // platformVersion?: string; // FARGATE ONLY. default is LATEST. Other options: 1.2.0, 1.1.0, 1.0.0 - //////////////////////////////////////////// + /** + * Fargate platform version to run this service on + * + * Unless you have specific compatibility requirements, you don't need to + * specify this. + * + * @default Latest + */ + platformVersion?: FargatePlatformVersion; } export abstract class BaseService extends cdk.Construct implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { public readonly dependencyElements: cdk.IDependable[]; - public readonly connections: ec2.Connections; + public abstract readonly connections: ec2.Connections; protected loadBalancers = new Array(); + protected networkConfiguration?: cloudformation.ServiceResource.NetworkConfigurationProperty; protected readonly abstract taskDef: BaseTaskDefinition; + protected _securityGroup?: ec2.SecurityGroupRef; private role?: iam.Role; private readonly resource: cloudformation.ServiceResource; constructor(parent: cdk.Construct, name: string, props: BaseServiceProps, additionalProps: any) { super(parent, name); - this.connections = new ec2.Connections({ - securityGroupRule: { - canInlineRule: false, - toEgressRuleJSON() { return {}; }, - toIngressRuleJSON() { return {}; }, - uniqueId: '' - }, - }); - - // this.taskDefinition = props.taskDefinition; - this.role = props.role; this.resource = new cloudformation.ServiceResource(this, "Service", { @@ -98,6 +88,8 @@ export abstract class BaseService extends cdk.Construct minimumHealthyPercent: props.minimumHealthyPercent }, role: new cdk.Token(() => this.role && this.role.roleArn), + networkConfiguration: this.networkConfiguration, + platformVersion: props.platformVersion, ...additionalProps }); @@ -105,7 +97,6 @@ export abstract class BaseService extends cdk.Construct } public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { - // FIXME: Security Groups this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, containerName: this.taskDef.defaultContainer!.name, @@ -127,6 +118,10 @@ export abstract class BaseService extends cdk.Construct return { targetType: elbv2.TargetType.SelfRegistering }; } + public get securityGroup(): ec2.SecurityGroupRef { + return this._securityGroup!; + } + protected createLoadBalancerRole() { if (!this.role) { this.role = new iam.Role(this, 'Role', { @@ -136,4 +131,57 @@ export abstract class BaseService extends cdk.Construct this.resource.addDependency(this.role); } } + + // tslint:disable-next-line:max-line-length + protected configureAwsVpcNetworking(vpc: ec2.VpcNetworkRef, assignPublicIp?: boolean, vpcPlacement?: ec2.VpcPlacementStrategy, securityGroup?: ec2.SecurityGroupRef) { + if (vpcPlacement === undefined) { + vpcPlacement = { subnetsToUse: assignPublicIp ? ec2.SubnetType.Public : ec2.SubnetType.Private }; + } + if (securityGroup === undefined) { + securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc }); + } + const subnets = vpc.subnets(vpcPlacement); + this._securityGroup = securityGroup; + + this.networkConfiguration = { + awsvpcConfiguration: { + assignPublicIp : assignPublicIp ? 'ENABLED' : 'DISABLED', + subnets: subnets.map(x => x.subnetId), + securityGroups: new cdk.Token(() => [securityGroup!.securityGroupId]), + } + }; + } } + +/** + * Fargate platform version + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html + */ +export enum FargatePlatformVersion { + /** + * The latest, recommended platform version + */ + Latest = 'LATEST', + + /** + * Version 1.2 + * + * Supports private registries. + */ + Version12 = '1.2.0', + + /** + * Version 1.1.0 + * + * Supports task metadata, health checks, service discovery. + */ + Version11 = '1.1.0', + + /** + * Initial release + * + * Based on Amazon Linux 2017.09. + */ + Version10 = '1.0.0', +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index cd3ff5b7be29f..c1df9645850b3 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -1,7 +1,9 @@ +import ec2 = require('@aws-cdk/aws-ec2'); import elb = require('@aws-cdk/aws-elasticloadbalancing'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; -import { BaseTaskDefinition } from '../base/base-task-definition'; +import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; +import { cloudformation } from '../ecs.generated'; import { EcsCluster } from './ecs-cluster'; import { EcsTaskDefinition } from './ecs-task-definition'; @@ -15,29 +17,156 @@ export interface EcsServiceProps extends BaseServiceProps { * Task Definition used for running tasks in the service */ taskDefinition: EcsTaskDefinition; + + /** + * In what subnets to place the task's ENIs + * + * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) + * + * @default Private subnets + */ + vpcPlacement?: ec2.VpcPlacementStrategy; + + /** + * Existing security group to use for the task's ENIs + * + * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) + * + * @default A new security group is created + */ + securityGroup?: ec2.SecurityGroupRef; + + /** + * Whether to start services on distinct instances + * + * @default true + */ + placeOnDistinctInstances?: boolean; + + /** + * Deploy exactly one task on each instance in your cluster. + * + * When using this strategy, do not specify a desired number of tasks or any + * task placement strategies. + * + * @default false + */ + daemon?: boolean; } export class EcsService extends BaseService implements elb.ILoadBalancerTarget { + public readonly connections: ec2.Connections; protected readonly taskDef: BaseTaskDefinition; private readonly taskDefinition: EcsTaskDefinition; + private readonly constraints: cloudformation.ServiceResource.PlacementConstraintProperty[]; + private readonly strategies: cloudformation.ServiceResource.PlacementStrategyProperty[]; + private readonly daemon: boolean; constructor(parent: cdk.Construct, name: string, props: EcsServiceProps) { + if (props.daemon && props.desiredCount !== undefined) { + throw new Error('Daemon mode launches one task on every instance. Don\'t supply desiredCount.'); + } + super(parent, name, props, { cluster: props.cluster.clusterName, taskDefinition: props.taskDefinition.taskDefinitionArn, - launchType: 'EC2' + launchType: 'EC2', + placementConstraints: new cdk.Token(() => this.constraints), + placementStrategies: new cdk.Token(() => this.strategies), + schedulingStrategy: props.daemon ? 'DAEMON' : 'REPLICA', }); + this.constraints = []; + this.strategies = []; + this.daemon = props.daemon || false; + + if (props.taskDefinition.networkMode === NetworkMode.AwsVpc) { + this.configureAwsVpcNetworking(props.cluster.vpc, false, props.vpcPlacement, props.securityGroup); + } else { + // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. + validateNoNetworkingProps(props); + this._securityGroup = props.cluster.autoScalingGroup.connections.securityGroup!; + } + + this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); this.taskDefinition = props.taskDefinition; this.taskDef = props.taskDefinition; + if (props.placeOnDistinctInstances) { + this.constraints.push({ type: 'distinctInstance' }); + } + if (!this.taskDefinition.defaultContainer) { throw new Error('A TaskDefinition must have at least one essential container'); } } + /** + * Place services only on instances matching the given query expression + * + * You can specify multiple expressions in one call. The tasks will only + * be placed on instances matching all expressions. + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cluster-query-language.html + */ + public placeOnMemberOf(...expressions: string[]) { + for (const expression of expressions) { + this.constraints.push({ type: 'memberOf', expression }); + } + } + + /** + * Try to place tasks spread across instance attributes. + * + * You can use one of the built-in attributes found on `BuiltInAttributes` + * or supply your own custom instance attributes. If more than one attribute + * is supplied, spreading is done in order. + * + * @default attributes instanceId + */ + public placeSpreadAcross(...fields: string[]) { + if (this.daemon) { + throw new Error("Can't configure spreading placement for a service with daemon=true"); + } + + if (fields.length === 0) { + fields = [BuiltInAttributes.InstanceId]; + } + for (const field of fields) { + this.strategies.push({ type: 'spread', field }); + } + } + + /** + * Try to place tasks on instances with the least amount of indicated resource available + * + * This ensures the total consumption of this resource is lowest. + */ + public placePackedBy(resource: BinPackResource) { + if (this.daemon) { + throw new Error("Can't configure packing placement for a service with daemon=true"); + } + + this.strategies.push({ type: 'binpack', field: resource }); + } + + /** + * Place tasks randomly across the available instances. + */ + public placeRandomly() { + if (this.daemon) { + throw new Error("Can't configure random placement for a service with daemon=true"); + } + + this.strategies.push({ type: 'random' }); + } + + /** + * Register this service as the target of a Classic Load Balancer + * + * Don't call this. Call `loadBalancer.addTarget()` instead. + */ public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { - // FIXME: Security Groups this.loadBalancers.push({ loadBalancerName: loadBalancer.loadBalancerName, containerName: this.taskDefinition.defaultContainer!.name, @@ -46,3 +175,56 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { this.createLoadBalancerRole(); } } + +function validateNoNetworkingProps(props: EcsServiceProps) { + if (props.vpcPlacement !== undefined || props.securityGroup !== undefined) { + throw new Error('vpcPlacement and securityGroup can only be used in AwsVpc networking mode'); + } +} + +/** + * Built-in container instance attributes + */ +export class BuiltInAttributes { + /** + * The Instance ID of the instance + */ + public static readonly InstanceId = 'instanceId'; + + /** + * The AZ where the instance is running + */ + public static readonly AvailabilityZone = 'attribute:ecs.availability-zone'; + + /** + * The AMI ID of the instance + */ + public static readonly AmiId = 'attribute:ecs.ami-id'; + + /** + * The instance type + */ + public static readonly InstanceType = 'attribute:ecs.instance-type'; + + /** + * The OS type + * + * Either 'linux' or 'windows'. + */ + public static readonly OsType = 'attribute:ecs.os-type'; +} + +/** + * Instance resource used for bin packing + */ +export enum BinPackResource { + /** + * Fill up hosts' CPU allocations first + */ + Cpu = 'cpu', + + /** + * Fill up hosts' memory allocations first + */ + Memory = 'memory', +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts index 02d9f3de2b894..5c70660c1e698 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts @@ -38,17 +38,23 @@ export interface EcsTaskDefinitionProps extends BaseTaskDefinitionProps { } export class EcsTaskDefinition extends BaseTaskDefinition { - private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[] = []; + public readonly networkMode: NetworkMode; + private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[]; constructor(parent: cdk.Construct, name: string, props: EcsTaskDefinitionProps) { + const networkMode = props.networkMode || NetworkMode.Bridge; + super(parent, name, props, { cpu: props.cpu, memoryMiB: props.memoryMiB, - networkMode: props.networkMode || NetworkMode.Bridge, + networkMode, requiresCompatibilities: [Compatibilities.Ec2], placementConstraints: new cdk.Token(() => this.placementConstraints) }); + this.networkMode = networkMode; + this.placementConstraints = []; + if (props.placementConstraints) { props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); } diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index dab41cc3c70df..d1a490c859c2b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -1,3 +1,4 @@ +import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; import { BaseTaskDefinition } from '../base/base-task-definition'; @@ -14,20 +15,45 @@ export interface FargateServiceProps extends BaseServiceProps { * Task Definition used for running tasks in the service */ taskDefinition: FargateTaskDefinition; + + /** + * Assign public IP addresses to each task + * + * @default false + */ + assignPublicIp?: boolean; + + /** + * In what subnets to place the task's ENIs + * + * @default Public subnet if assignPublicIp, private subnets otherwise + */ + vpcPlacement?: ec2.VpcPlacementStrategy; + + /** + * Existing security group to use for the tasks + * + * @default A new security group is created + */ + securityGroup?: ec2.SecurityGroupRef; } export class FargateService extends BaseService { + public readonly connections: ec2.Connections; + public readonly taskDefinition: FargateTaskDefinition; protected readonly taskDef: BaseTaskDefinition; - private readonly taskDefinition: FargateTaskDefinition; constructor(parent: cdk.Construct, name: string, props: FargateServiceProps) { super(parent, name, props, { cluster: props.cluster.clusterName, taskDefinition: props.taskDefinition.taskDefinitionArn, - launchType: 'FARGATE' + launchType: 'FARGATE', }); - if (!this.taskDefinition.defaultContainer) { + this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcPlacement, props.securityGroup); + this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); + + if (!props.taskDefinition.defaultContainer) { throw new Error('A TaskDefinition must have at least one essential container'); } From 97083dc98b7b2cf8b4e02b883fa954ab4002c219 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 15:01:58 +0200 Subject: [PATCH 041/140] Add Ulimits --- .../hello-cdk-ecs/index.ts | 6 +++ .../aws-ecs/lib/container-definition.ts | 45 ++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 6a0f170c15f2c..1300b0fc15bc6 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -60,6 +60,12 @@ class BonjourECS extends cdk.Stack { container.linuxParameters.sharedMemorySize = 65535 container.linuxParameters.initProcessEnabled = true + container.addUlimits({ + name: ecs.UlimitName.Core, + softLimit: 1234, + hardLimit: 1234, + }); + taskDefinition.addContainer(container); new ecs.EcsService(this, "EcsService", { diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index f6ab4dfe0c576..5abc092fd9c1e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -174,6 +174,8 @@ export class ContainerDefinition extends cdk.Construct { public readonly linuxParameters = new LinuxParameters(); + public readonly ulimits = new Array(); + public readonly essential: boolean; private readonly links = new Array(); @@ -195,6 +197,10 @@ export class ContainerDefinition extends cdk.Construct { } } + public addUlimits(...ulimits: Ulimits) { + this.ulimits.push(...ulimits); + } + /** * Mark this ContainerDefinition as using an ECR image */ @@ -231,7 +237,7 @@ export class ContainerDefinition extends cdk.Construct { privileged: this.props.privileged, readonlyRootFilesystem: this.props.readonlyRootFilesystem, repositoryCredentials: undefined, // FIXME - ulimits: [], // FIXME + ulimits: this.ulimits.map(renderUlimit), user: this.props.user, volumesFrom: [], // FIXME workingDirectory: this.props.workingDirectory, @@ -334,3 +340,40 @@ function getHealthCheckCommand(hc: HealthCheck): string[] { return hcCommand.concat(cmd); } + +/** + * Container ulimits. Correspond to ulimits options on docker run. + * + * NOTE: Does not work for Windows containers. + */ +export interface Ulimits { + name: UlimitName, + softLimit: number, + hardLimit: number, +} + +export enum UlimitName { + Core = "core", + Cpu = "cpu", + Data = "data", + Fsize = "fsize", + Locks = "locks", + Memlock = "memlock", + Msgqueue = "msgqueue", + Nice = "nice", + Nofile = "nofile", + Nproc = "nproc", + Rss = "rss", + Rtprio = "rtprio", + Rttime = "rttime", + Sigpending = "sigpending", + Stack = "stack" +} + +function renderUlimit(ulimit: Ulimit): cloudformation.TaskDefinitionResource.UlimitProperty { + return { + name: ulimit.name, + softLimit: ulimit.softLimit, + hardLimit: ulimit.hardLimit, + }; +} From a3e361562e4e8a4eaa3ce6dbaeff05a7269ab2ac Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 15:29:16 +0200 Subject: [PATCH 042/140] Fix log driver --- packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts index 2b8e9c5882d2f..eb7c1344b5dda 100644 --- a/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/log-driver.ts @@ -8,5 +8,5 @@ export abstract class LogDriver extends cdk.Construct { /** * Return the log driver CloudFormation JSON */ - public abstract toLogDriverJson(): cloudformation.TaskDefinitionResource.LogConfigurationProperty; + public abstract renderLogDriver(): cloudformation.TaskDefinitionResource.LogConfigurationProperty; } From b913f0eac4bb0ad8ca1680a7e6e21d7bd456293e Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 15:38:15 +0200 Subject: [PATCH 043/140] Make add* functions consistently plural Except for `addTmpfs`, bc `addTmpfses` is just silly --- .../hello-cdk-ecs/index.ts | 5 +++-- .../aws-ecs/lib/container-definition.ts | 6 +++--- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 18 +++++++++--------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 1300b0fc15bc6..1a19d8a1c3f09 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -43,9 +43,10 @@ class BonjourECS extends cdk.Stack { essential: true }); - container.linuxParameters.addCapability(ecs.Capability.All); + container.linuxParameters.addCapabilities(ecs.Capability.All); + container.linuxParameters.dropCapabilities(ecs.Capability.Chown); - container.linuxParameters.addDevice({ + container.linuxParameters.addDevices({ containerPath: "/pudding", hostPath: "/dev/sda", permissions: [ecs.DevicePermission.Read] diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 5abc092fd9c1e..6f91984e9b469 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -174,7 +174,7 @@ export class ContainerDefinition extends cdk.Construct { public readonly linuxParameters = new LinuxParameters(); - public readonly ulimits = new Array(); + public readonly ulimits = new Array(); public readonly essential: boolean; @@ -197,7 +197,7 @@ export class ContainerDefinition extends cdk.Construct { } } - public addUlimits(...ulimits: Ulimits) { + public addUlimits(...ulimits: Ulimit[]) { this.ulimits.push(...ulimits); } @@ -346,7 +346,7 @@ function getHealthCheckCommand(hc: HealthCheck): string[] { * * NOTE: Does not work for Windows containers. */ -export interface Ulimits { +export interface Ulimit { name: UlimitName, softLimit: number, hardLimit: number, diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index 21a149310b65b..d7d0dabd36781 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -5,9 +5,9 @@ export class LinuxParameters { public sharedMemorySize?: number; - private readonly addCapabilities: Capability[] = []; + private readonly capAdd: Capability[] = []; - private readonly dropCapabilities: Capability[] = []; + private readonly capDrop: Capability[] = []; private readonly devices: Device[] = []; @@ -16,15 +16,15 @@ export class LinuxParameters { /** * AddCapability only works with EC2 launch type */ - public addCapability(...cap: Capability[]) { - this.addCapabilities.push(...cap); + public addCapabilities(...cap: Capability[]) { + this.capAdd.push(...cap); } - public dropCapability(...cap: Capability[]) { - this.dropCapabilities.push(...cap); + public dropCapabilities(...cap: Capability[]) { + this.capDrop.push(...cap); } - public addDevice(...device: Device[]) { + public addDevices(...device: Device[]) { this.devices.push(...device); } @@ -37,8 +37,8 @@ export class LinuxParameters { initProcessEnabled: this.initProcessEnabled, sharedMemorySize: this.sharedMemorySize, capabilities: { - add: this.addCapabilities, - drop: this.dropCapabilities, + add: this.capAdd, + drop: this.capDrop, }, devices: this.devices.map(renderDevice), tmpfs: this.tmpfs.map(renderTmpfs) From fd21c0df07574dac724cc0738520c37b20985acc Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 2 Oct 2018 15:48:10 +0200 Subject: [PATCH 044/140] Make load balancers respect network mode --- .../aws-autoscaling/lib/auto-scaling-group.ts | 4 +- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 55 +++++++++++++------ .../aws-ecs/lib/base/base-task-definition.ts | 3 +- .../aws-ecs/lib/container-definition.ts | 5 +- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 9 ++- .../aws-elasticloadbalancingv2/README.md | 12 ++-- .../lib/shared/base-target-group.ts | 13 ++--- .../lib/shared/enums.ts | 5 -- .../test/helpers.ts | 4 +- 9 files changed, 66 insertions(+), 44 deletions(-) diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index acf23e55ec05f..c10e8b6f0edad 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -276,7 +276,7 @@ export class AutoScalingGroup extends cdk.Construct implements cdk.ITaggable, el public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { this.targetGroupArns.push(targetGroup.targetGroupArn); targetGroup.registerConnectable(this); - return { targetType: elbv2.TargetType.SelfRegistering }; + return { targetType: elbv2.TargetType.Instance }; } /** @@ -284,7 +284,7 @@ export class AutoScalingGroup extends cdk.Construct implements cdk.ITaggable, el */ public attachToNetworkTargetGroup(targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { this.targetGroupArns.push(targetGroup.targetGroupArn); - return { targetType: elbv2.TargetType.SelfRegistering }; + return { targetType: elbv2.TargetType.Instance }; } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 1205f66642164..c00b0b40c0dd5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -2,7 +2,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { BaseTaskDefinition } from '../base/base-task-definition'; +import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; export interface BaseServiceProps { @@ -97,25 +97,19 @@ export abstract class BaseService extends cdk.Construct } public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { - this.loadBalancers.push({ - targetGroupArn: targetGroup.targetGroupArn, - containerName: this.taskDef.defaultContainer!.name, - containerPort: this.taskDef.defaultContainer!.loadBalancerPort(false), - }); - this.createLoadBalancerRole(); + const ret = this.attachToELBv2(targetGroup); + + // Open up security groups. For dynamic port mapping, we won't know the port range + // in advance so we need to open up all ports. + const port = this.instancePort; + const portRange = port === 0 ? EPHEMERAL_PORT_RANGE : new ec2.TcpPort(port); + targetGroup.registerConnectable(this, portRange); - return { targetType: elbv2.TargetType.SelfRegistering }; + return ret; } public attachToNetworkTargetGroup(targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { - this.loadBalancers.push({ - targetGroupArn: targetGroup.targetGroupArn, - containerName: this.taskDef.defaultContainer!.name, - containerPort: this.taskDef.defaultContainer!.loadBalancerPort(false), - }); - this.createLoadBalancerRole(); - - return { targetType: elbv2.TargetType.SelfRegistering }; + return this.attachToELBv2(targetGroup); } public get securityGroup(): ec2.SecurityGroupRef { @@ -151,8 +145,37 @@ export abstract class BaseService extends cdk.Construct } }; } + + private attachToELBv2(targetGroup: elbv2.ITargetGroup): elbv2.LoadBalancerTargetProps { + if (this.taskDef.networkMode === NetworkMode.None) { + throw new Error("Cannot use a load balancer if NetworkMode is None. Use Host or AwsVpc instead."); + } + + this.loadBalancers.push({ + targetGroupArn: targetGroup.targetGroupArn, + containerName: this.taskDef.defaultContainer!.name, + containerPort: this.instancePort, + }); + this.createLoadBalancerRole(); + + return { targetType: elbv2.TargetType.Ip }; + } + + /** + * Return the port on which the instance will be listening + * + * Returns 0 if the networking mode implies dynamic port allocation. + */ + private get instancePort() { + return this.taskDef.networkMode === NetworkMode.Bridge ? 0 : this.taskDef.defaultContainer!.instancePort; + } } +/** + * The port range to open up for dynamic port mapping + */ +const EPHEMERAL_PORT_RANGE = new ec2.TcpPortRange(32768, 65535); + /** * Fargate platform version * diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index 46b99085c8b15..91fdc843eec8d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -34,10 +34,11 @@ export interface BaseTaskDefinitionProps { volumes?: Volume[]; } -export class BaseTaskDefinition extends cdk.Construct { +export abstract class BaseTaskDefinition extends cdk.Construct { public readonly family: string; public readonly taskDefinitionArn: string; public readonly taskRole: iam.Role; + public abstract readonly networkMode: NetworkMode; /** * Default container for this task diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index f6ab4dfe0c576..a92e404c9a5b0 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -206,7 +206,10 @@ export class ContainerDefinition extends cdk.Construct { return this._usesEcrImages; } - public loadBalancerPort(_classicLB: boolean): number { + /** + * Return the instance port that the container will be listening on + */ + public get instancePort(): number { return 0; } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index c1df9645850b3..9daf768230766 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -167,10 +167,17 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { * Don't call this. Call `loadBalancer.addTarget()` instead. */ public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { + if (this.taskDefinition.networkMode === NetworkMode.Bridge) { + throw new Error("Cannot use a Classic Load Balancer if NetworkMode is Bridge. Use Host or AwsVpc instead."); + } + if (this.taskDefinition.networkMode === NetworkMode.None) { + throw new Error("Cannot use a load balancer if NetworkMode is None. Use Host or AwsVpc instead."); + } + this.loadBalancers.push({ loadBalancerName: loadBalancer.loadBalancerName, containerName: this.taskDefinition.defaultContainer!.name, - containerPort: this.taskDefinition.defaultContainer!.loadBalancerPort(true), + containerPort: this.taskDefinition.defaultContainer!.instancePort, }); this.createLoadBalancerRole(); } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index b7110d5ca5875..18d6cab616d5f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -179,18 +179,14 @@ load balancing target: public attachToApplicationTargetGroup(targetGroup: ApplicationTargetGroup): LoadBalancerTargetProps { targetGroup.registerConnectable(...); return { - targetType: TargetType.Instance | TargetType.Ip | TargetType.SelfRegistering, + targetType: TargetType.Instance | TargetType.Ip targetJson: { id: ..., port: ... }, }; } ``` - -`targetType` should be one of `Instance` or `Ip` if the target can be directly -added to the target group, or `SelfRegistering` if the target will register new -instances with the load balancer at some later point. - -If the `targetType` is `Instance` or `Ip`, `targetJson` should contain the `id` -of the target (either instance ID or IP address depending on the type) and +`targetType` should be one of `Instance` or `Ip`. If the target can be +directly added to the target group, `targetJson` should contain the `id` of +the target (either instance ID or IP address depending on the type) and optionally a `port` or `availabilityZone` override. Application load balancer targets can call `registerConnectable()` on the diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 696ce8108d865..356456de0ab26 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -238,15 +238,10 @@ export abstract class BaseTargetGroup extends cdk.Construct implements ITargetGr * Register the given load balancing target as part of this group */ protected addLoadBalancerTarget(props: LoadBalancerTargetProps) { - if ((props.targetType === TargetType.SelfRegistering) !== (props.targetJson === undefined)) { - throw new Error('Load balancing target should specify targetJson if and only if TargetType is not SelfRegistering'); - } - if (props.targetType !== TargetType.SelfRegistering) { - if (this.targetType !== undefined && this.targetType !== props.targetType) { - throw new Error(`Already have a of type '${this.targetType}', adding '${props.targetType}'; make all targets the same type.`); - } - this.targetType = props.targetType; + if (this.targetType !== undefined && this.targetType !== props.targetType) { + throw new Error(`Already have a of type '${this.targetType}', adding '${props.targetType}'; make all targets the same type.`); } + this.targetType = props.targetType; if (props.targetJson) { this.targetsJson.push(props.targetJson); @@ -290,6 +285,8 @@ export interface LoadBalancerTargetProps { /** * JSON representing the target's direct addition to the TargetGroup list + * + * May be omitted if the target is going to register itself later. */ targetJson?: any; } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts index 8888c23e0e949..f043272ab139d 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts @@ -109,9 +109,4 @@ export enum TargetType { * Targets identified by IP address */ Ip = 'ip', - - /** - * A target that will register itself with the target group - */ - SelfRegistering = 'self-registering', } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts index 75d35f74da063..77fec08c04824 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts @@ -17,10 +17,10 @@ export class FakeSelfRegisteringTarget extends cdk.Construct implements elbv2.IA public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { targetGroup.registerConnectable(this); - return { targetType: elbv2.TargetType.SelfRegistering }; + return { targetType: elbv2.TargetType.Instance }; } public attachToNetworkTargetGroup(_targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { - return { targetType: elbv2.TargetType.SelfRegistering }; + return { targetType: elbv2.TargetType.Instance }; } } From 187f62b18a807a2e5914dc1be1974da8b4563510 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 15:38:15 +0200 Subject: [PATCH 045/140] Make add* functions consistently plural Except for `addTmpfs`, bc `addTmpfses` is just silly --- .../hello-cdk-ecs/index.ts | 5 +++-- .../aws-ecs/lib/container-definition.ts | 6 +++--- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 20 +++++++++---------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 1300b0fc15bc6..1a19d8a1c3f09 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -43,9 +43,10 @@ class BonjourECS extends cdk.Stack { essential: true }); - container.linuxParameters.addCapability(ecs.Capability.All); + container.linuxParameters.addCapabilities(ecs.Capability.All); + container.linuxParameters.dropCapabilities(ecs.Capability.Chown); - container.linuxParameters.addDevice({ + container.linuxParameters.addDevices({ containerPath: "/pudding", hostPath: "/dev/sda", permissions: [ecs.DevicePermission.Read] diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 5abc092fd9c1e..6f91984e9b469 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -174,7 +174,7 @@ export class ContainerDefinition extends cdk.Construct { public readonly linuxParameters = new LinuxParameters(); - public readonly ulimits = new Array(); + public readonly ulimits = new Array(); public readonly essential: boolean; @@ -197,7 +197,7 @@ export class ContainerDefinition extends cdk.Construct { } } - public addUlimits(...ulimits: Ulimits) { + public addUlimits(...ulimits: Ulimit[]) { this.ulimits.push(...ulimits); } @@ -346,7 +346,7 @@ function getHealthCheckCommand(hc: HealthCheck): string[] { * * NOTE: Does not work for Windows containers. */ -export interface Ulimits { +export interface Ulimit { name: UlimitName, softLimit: number, hardLimit: number, diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index 21a149310b65b..2d3af2136fd7c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -5,26 +5,26 @@ export class LinuxParameters { public sharedMemorySize?: number; - private readonly addCapabilities: Capability[] = []; + private readonly capAdd: Capability[] = []; - private readonly dropCapabilities: Capability[] = []; + private readonly capDrop: Capability[] = []; private readonly devices: Device[] = []; private readonly tmpfs: Tmpfs[] = []; /** - * AddCapability only works with EC2 launch type + * AddCapabilities only works with EC2 launch type */ - public addCapability(...cap: Capability[]) { - this.addCapabilities.push(...cap); + public addCapabilities(...cap: Capability[]) { + this.capAdd.push(...cap); } - public dropCapability(...cap: Capability[]) { - this.dropCapabilities.push(...cap); + public dropCapabilities(...cap: Capability[]) { + this.capDrop.push(...cap); } - public addDevice(...device: Device[]) { + public addDevices(...device: Device[]) { this.devices.push(...device); } @@ -37,8 +37,8 @@ export class LinuxParameters { initProcessEnabled: this.initProcessEnabled, sharedMemorySize: this.sharedMemorySize, capabilities: { - add: this.addCapabilities, - drop: this.dropCapabilities, + add: this.capAdd, + drop: this.capDrop, }, devices: this.devices.map(renderDevice), tmpfs: this.tmpfs.map(renderTmpfs) From 6912e3f484fb32a0ec887c32b740bb6777118dbf Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 16:18:49 +0200 Subject: [PATCH 046/140] Add portMappings to ContainerDefinition --- .../hello-cdk-ecs/index.ts | 12 ++++++--- .../aws-ecs/lib/container-definition.ts | 27 ++++++++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 1a19d8a1c3f09..7a2d45258be08 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -47,13 +47,13 @@ class BonjourECS extends cdk.Stack { container.linuxParameters.dropCapabilities(ecs.Capability.Chown); container.linuxParameters.addDevices({ - containerPath: "/pudding", - hostPath: "/dev/sda", + containerPath: "/dev/pudding", + hostPath: "/dev/clyde", permissions: [ecs.DevicePermission.Read] }); container.linuxParameters.addTmpfs({ - containerPath: "/pudding", + containerPath: "/dev/sda", size: 12345, mountOptions: [ecs.TmpfsMountOption.Ro] }); @@ -67,6 +67,12 @@ class BonjourECS extends cdk.Stack { hardLimit: 1234, }); + container.addPortMappings({ + containerPort: 80 + hostPort: 80, + protocol: ecs.Protocol.Tcp, + }); + taskDefinition.addContainer(container); new ecs.EcsService(this, "EcsService", { diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 6f91984e9b469..d73bb18845b5d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -174,6 +174,8 @@ export class ContainerDefinition extends cdk.Construct { public readonly linuxParameters = new LinuxParameters(); + public readonly portMappings = new Array(); + public readonly ulimits = new Array(); public readonly essential: boolean; @@ -197,6 +199,10 @@ export class ContainerDefinition extends cdk.Construct { } } + public addPortMappings(...portMappings: PortMapping[]) { + this.portMappings.push(...portMappings); + } + public addUlimits(...ulimits: Ulimit[]) { this.ulimits.push(...ulimits); } @@ -233,7 +239,7 @@ export class ContainerDefinition extends cdk.Construct { memoryReservation: this.props.memoryReservationMiB, mountPoints: [], // FIXME name: this.props.name, - portMappings: [], // FIXME + portMappings: this.portMappings.map(renderPortMapping), privileged: this.props.privileged, readonlyRootFilesystem: this.props.readonlyRootFilesystem, repositoryCredentials: undefined, // FIXME @@ -377,3 +383,22 @@ function renderUlimit(ulimit: Ulimit): cloudformation.TaskDefinitionResource.Uli hardLimit: ulimit.hardLimit, }; } + +export interface PortMapping { + containerPort?: number, + hostPort?: number, + protocol: Protocol +} + +export enum Protocol { + Tcp = "tcp", + Udp = "udp", +} + +function renderPortMapping(pm: PortMapping): cloudformation.TaskDefinitionResource.PortMappingProperty { + return { + containerPort: pm.containerPort, + hostPort: pm.hostPort, + protocol: pm.protocol, + }; +} From 2bf2d57c3c32b4836e861e67469e46344e6cd093 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 16:19:17 +0200 Subject: [PATCH 047/140] Missing semicolons --- packages/@aws-cdk/aws-ecs/lib/container-definition.ts | 1 + packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index d73bb18845b5d..28646d85a9951 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -384,6 +384,7 @@ function renderUlimit(ulimit: Ulimit): cloudformation.TaskDefinitionResource.Uli }; } +// TODO: add default? export interface PortMapping { containerPort?: number, hostPort?: number, diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index 2d3af2136fd7c..bdc7c9a7c0089 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -57,7 +57,7 @@ function renderDevice(device: Device): cloudformation.TaskDefinitionResource.Dev containerPath: device.containerPath, hostPath: device.hostPath, permissions: device.permissions - } + }; } export interface Tmpfs { @@ -71,7 +71,7 @@ function renderTmpfs(tmpfs: Tmpfs): cloudformation.TaskDefinitionResource.TmpfsP containerPath: tmpfs.containerPath, size: tmpfs.size, mountOptions: tmpfs.mountOptions - } + }; } export enum Capability { From f477d8d37bd65c32242a8ca055f3f4f30009218f Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 16:37:06 +0200 Subject: [PATCH 048/140] Add MountPoints to ContainerDefinition --- .../hello-cdk-ecs/index.ts | 13 ++++++++++- .../aws-ecs/lib/container-definition.ts | 22 ++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 7a2d45258be08..fbaf875d56d43 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -68,11 +68,22 @@ class BonjourECS extends cdk.Stack { }); container.addPortMappings({ - containerPort: 80 + containerPort: 80, hostPort: 80, protocol: ecs.Protocol.Tcp, }); + container.addMountPoints({ + containerPath: '/tmp/cache', + sourceVolume: 'volume-1', + readOnly: true, + }, { + containerPath: './cache', + sourceVolume: 'volume-2', + readOnly: true, + }); + + taskDefinition.addContainer(container); new ecs.EcsService(this, "EcsService", { diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 28646d85a9951..7e8fcdc04a63d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -174,6 +174,8 @@ export class ContainerDefinition extends cdk.Construct { public readonly linuxParameters = new LinuxParameters(); + public readonly mountPoints = new Array(); + public readonly portMappings = new Array(); public readonly ulimits = new Array(); @@ -199,6 +201,10 @@ export class ContainerDefinition extends cdk.Construct { } } + public addMountPoints(...mountPoints: MountPoint[]) { + this.mountPoints.push(...mountPoints); + } + public addPortMappings(...portMappings: PortMapping[]) { this.portMappings.push(...portMappings); } @@ -237,7 +243,7 @@ export class ContainerDefinition extends cdk.Construct { image: this.props.image.imageName, memory: this.props.memoryMiB, memoryReservation: this.props.memoryReservationMiB, - mountPoints: [], // FIXME + mountPoints: this.mountPoints.map(renderMountPoint), name: this.props.name, portMappings: this.portMappings.map(renderPortMapping), privileged: this.props.privileged, @@ -403,3 +409,17 @@ function renderPortMapping(pm: PortMapping): cloudformation.TaskDefinitionResour protocol: pm.protocol, }; } + +export interface MountPoint { + containerPath: string, + readOnly: boolean, + sourceVolume: string, +} + +function renderMountPoint(mp: MountPoint): cloudformation.TaskDefinitionResource.MountPointProperty { + return { + containerPath: mp.containerPath, + readOnly: mp.readOnly, + sourceVolume: mp.sourceVolume, + }; +} From b947f5114a490997a53cbd28cea14f7c33269404 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 2 Oct 2018 16:50:51 +0200 Subject: [PATCH 049/140] Change 'addContainer' to take the props and return ContainerDefinition. Add (start of) integ test. --- .../hello-cdk-ecs/index.ts | 9 ++--- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- .../aws-ecs/lib/base/base-task-definition.ts | 6 ++-- .../aws-ecs/lib/container-definition.ts | 22 +++++------- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 2 +- .../aws-ecs/lib/ecs/ecs-task-definition.ts | 21 +----------- .../lib/fargate/fargate-task-definition.ts | 12 ++++--- .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 4 +-- .../test/fargate/integ.lb-bridge-nw.ts | 34 +++++++++++++++++++ 9 files changed, 62 insertions(+), 50 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 1a19d8a1c3f09..2ed542a83ac6c 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -35,8 +35,7 @@ class BonjourECS extends cdk.Stack { family: "ecs-task-definition", }); - const container = new ecs.ContainerDefinition(this, 'Container', { - name: "web", + const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), cpu: 1024, memoryMiB: 512, @@ -58,8 +57,8 @@ class BonjourECS extends cdk.Stack { mountOptions: [ecs.TmpfsMountOption.Ro] }); - container.linuxParameters.sharedMemorySize = 65535 - container.linuxParameters.initProcessEnabled = true + container.linuxParameters.sharedMemorySize = 65535; + container.linuxParameters.initProcessEnabled = true; container.addUlimits({ name: ecs.UlimitName.Core, @@ -67,8 +66,6 @@ class BonjourECS extends cdk.Stack { hardLimit: 1234, }); - taskDefinition.addContainer(container); - new ecs.EcsService(this, "EcsService", { cluster, taskDefinition, diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index c00b0b40c0dd5..c06e03cfd0d61 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -153,7 +153,7 @@ export abstract class BaseService extends cdk.Construct this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, - containerName: this.taskDef.defaultContainer!.name, + containerName: this.taskDef.defaultContainer!.id, containerPort: this.instancePort, }); this.createLoadBalancerRole(); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index 91fdc843eec8d..7b4159a226048 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -1,6 +1,6 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { ContainerDefinition } from '../container-definition'; +import { ContainerDefinition, ContainerDefinitionProps } from '../container-definition'; import { cloudformation } from '../ecs.generated'; export interface BaseTaskDefinitionProps { @@ -88,7 +88,8 @@ export abstract class BaseTaskDefinition extends cdk.Construct { /** * Add a container to this task */ - public addContainer(container: ContainerDefinition) { + public addContainer(id: string, props: ContainerDefinitionProps) { + const container = new ContainerDefinition(this, id, props); this.containers.push(container); if (container.usesEcrImages) { this.generateExecutionRole(); @@ -96,6 +97,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { if (this.defaultContainer === undefined && container.essential) { this.defaultContainer = container; } + return container; } private addVolume(volume: Volume) { diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index a0921ea407584..22442085b73ec 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -5,11 +5,6 @@ import { LinuxParameters } from './linux-parameters'; import { LogDriver } from './log-drivers/log-driver'; export interface ContainerDefinitionProps { - /** - * A name for the container. - */ - name: string; - /** * The image to use for a container. * @@ -122,8 +117,10 @@ export interface ContainerDefinitionProps { * * If your container attempts to exceed the allocated memory, the container * is terminated. + * + * At least one of memoryLimitMiB and memoryReservationMiB is required. */ - memoryMiB?: number; + memoryLimitMiB?: number; /** * The soft limit (in MiB) of memory to reserve for the container. @@ -132,6 +129,8 @@ export interface ContainerDefinitionProps { * container memory within the limit. If the container requires more memory, * it can consume up to the value specified by the Memory property or all of * the available memory on the container instance—whichever comes first. + * + * At least one of memoryLimitMiB and memoryReservationMiB is required. */ memoryReservationMiB?: number; @@ -170,8 +169,6 @@ export interface ContainerDefinitionProps { } export class ContainerDefinition extends cdk.Construct { - public readonly name: string; - public readonly linuxParameters = new LinuxParameters(); public readonly ulimits = new Array(); @@ -184,16 +181,15 @@ export class ContainerDefinition extends cdk.Construct { constructor(parent: cdk.Construct, id: string, private readonly props: ContainerDefinitionProps) { super(parent, id); - this.name = props.name; this.essential = props.essential !== undefined ? props.essential : true; props.image.bind(this); } public addLink(container: ContainerDefinition, alias?: string) { if (alias !== undefined) { - this.links.push(`${container.name}:${alias}`); + this.links.push(`${container.id}:${alias}`); } else { - this.links.push(`${container.name}`); + this.links.push(`${container.id}`); } } @@ -232,10 +228,10 @@ export class ContainerDefinition extends cdk.Construct { essential: this.essential, hostname: this.props.hostname, image: this.props.image.imageName, - memory: this.props.memoryMiB, + memory: this.props.memoryLimitMiB, memoryReservation: this.props.memoryReservationMiB, mountPoints: [], // FIXME - name: this.props.name, + name: this.id, portMappings: [], // FIXME privileged: this.props.privileged, readonlyRootFilesystem: this.props.readonlyRootFilesystem, diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 9daf768230766..f9f9309558212 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -176,7 +176,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { this.loadBalancers.push({ loadBalancerName: loadBalancer.loadBalancerName, - containerName: this.taskDefinition.defaultContainer!.name, + containerName: this.taskDefinition.defaultContainer!.id, containerPort: this.taskDefinition.defaultContainer!.instancePort, }); this.createLoadBalancerRole(); diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts index 5c70660c1e698..19659836781d9 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts @@ -3,23 +3,6 @@ import { BaseTaskDefinition, BaseTaskDefinitionProps, Compatibilities, NetworkMo import { cloudformation } from '../ecs.generated'; export interface EcsTaskDefinitionProps extends BaseTaskDefinitionProps { - /** - * The number of cpu units used by the task. If using the EC2 launch type, - * this field is optional. Supported values are between 128 CPU units - * (0.125 vCPUs) and 10240 CPU units (10 vCPUs). - * - * @default 256 - */ - cpu?: string; - - /** - * The amount (in MiB) of memory used by the task. If using the EC2 launch type, this field is optional and any value - * can be used. - * - * @default 512 - */ - memoryMiB?: string; - /** * The Docker networking mode to use for the containers in the task. * @@ -41,12 +24,10 @@ export class EcsTaskDefinition extends BaseTaskDefinition { public readonly networkMode: NetworkMode; private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[]; - constructor(parent: cdk.Construct, name: string, props: EcsTaskDefinitionProps) { + constructor(parent: cdk.Construct, name: string, props: EcsTaskDefinitionProps = {}) { const networkMode = props.networkMode || NetworkMode.Bridge; super(parent, name, props, { - cpu: props.cpu, - memoryMiB: props.memoryMiB, networkMode, requiresCompatibilities: [Compatibilities.Ec2], placementConstraints: new cdk.Token(() => this.placementConstraints) diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index a1ac36c29b47c..7e445d1ad2efe 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -13,7 +13,7 @@ export interface FargateTaskDefinitionProps extends BaseTaskDefinitionProps { * * @default 256 */ - cpu: string; + cpu?: string; /** * The amount (in MiB) of memory used by the task. @@ -33,14 +33,16 @@ export interface FargateTaskDefinitionProps extends BaseTaskDefinitionProps { * * @default 512 */ - memoryMiB: string; + memoryMiB?: string; } export class FargateTaskDefinition extends BaseTaskDefinition { - constructor(parent: cdk.Construct, name: string, props: FargateTaskDefinitionProps) { + public readonly networkMode = NetworkMode.AwsVpc; + + constructor(parent: cdk.Construct, name: string, props: FargateTaskDefinitionProps = {}) { super(parent, name, props, { - cpu: props.cpu, - memoryMiB: props.memoryMiB, + cpu: props.cpu || '256', + memoryMiB: props.memoryMiB || '512', networkMode: NetworkMode.AwsVpc, requiresCompatibilities: [Compatibilities.Fargate] }); diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index d7d0dabd36781..80b46b5d0eb9b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -57,7 +57,7 @@ function renderDevice(device: Device): cloudformation.TaskDefinitionResource.Dev containerPath: device.containerPath, hostPath: device.hostPath, permissions: device.permissions - } + }; } export interface Tmpfs { @@ -71,7 +71,7 @@ function renderTmpfs(tmpfs: Tmpfs): cloudformation.TaskDefinitionResource.TmpfsP containerPath: tmpfs.containerPath, size: tmpfs.size, mountOptions: tmpfs.mountOptions - } + }; } export enum Capability { diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts new file mode 100644 index 0000000000000..76be274f5d3f5 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts @@ -0,0 +1,34 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +// import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +// import ecs = require('../../lib'); + +const app = new cdk.App(process.argv); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); + +const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); +Array.isArray(vpc); +/* +const cluster = new ecs.FargateCluster(stack, 'EcsCluster', { vpc }); + +const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef'); +taskDefinition.addContainer('web', { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + cpu: 256, + memoryMiB: 512, +}); + +const service = new ecs.FargateService(stack, "Service", { + cluster, + taskDefinition, +}); + +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); +const listener = lb.addListener('PublicListener', { port: 80 }); +listener.addTargets('ECS', { + port: 80, + targets: [service] +}); +*/ + +process.stdout.write(app.run()); \ No newline at end of file From 30cb925f2123027962d11aca641b738631b8861d Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 16:57:14 +0200 Subject: [PATCH 050/140] Add VolumesFrom to ContainerDefinition --- .../hello-cdk-ecs/index.ts | 4 ++++ .../aws-ecs/lib/container-definition.ts | 24 +++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index ad387476873e2..97ba362de1b9c 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -82,6 +82,10 @@ class BonjourECS extends cdk.Stack { readOnly: true, }); + container.addVolumesFrom({ + sourceContainer: 'web', + readOnly: true, + }); new ecs.EcsService(this, "EcsService", { cluster, diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index c73c198fa6d49..3342499326f12 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -175,6 +175,8 @@ export class ContainerDefinition extends cdk.Construct { public readonly portMappings = new Array(); + public readonly volumesFrom = new Array(); + public readonly ulimits = new Array(); public readonly essential: boolean; @@ -209,6 +211,10 @@ export class ContainerDefinition extends cdk.Construct { this.ulimits.push(...ulimits); } + public addVolumesFrom(...volumesFrom: VolumeFrom[]) { + this.volumesFrom.push(...volumesFrom); + } + /** * Mark this ContainerDefinition as using an ECR image */ @@ -250,7 +256,7 @@ export class ContainerDefinition extends cdk.Construct { repositoryCredentials: undefined, // FIXME ulimits: this.ulimits.map(renderUlimit), user: this.props.user, - volumesFrom: [], // FIXME + volumesFrom: this.volumesFrom.map(renderVolumeFrom), workingDirectory: this.props.workingDirectory, logConfiguration: this.props.logging && this.props.logging.renderLogDriver(), environment: this.props.environment && renderKV(this.props.environment, 'name', 'value'), @@ -310,10 +316,6 @@ export interface HealthCheck { timeout?: number; } -// mountPoints?: mountPoint[]; -// portMappings?: portMapping[]; -// volumesFrom?: volumeFrom[]; - function renderKV(env: {[key: string]: string}, keyName: string, valueName: string): any { const ret = []; for (const [key, value] of Object.entries(env)) { @@ -422,3 +424,15 @@ function renderMountPoint(mp: MountPoint): cloudformation.TaskDefinitionResource sourceVolume: mp.sourceVolume, }; } + +export interface VolumeFrom { + sourceContainer: string, + readOnly: boolean, +} + +function renderVolumeFrom(vf: VolumeFrom): cloudformation.TaskDefinitionResource.VolumeFromProperty { + return { + sourceContainer: vf.sourceContainer, + readOnly: vf.readOnly, + }; +} From c64e184a35c0a938beb5e54510afe32b6499e5ac Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 3 Oct 2018 12:06:06 +0200 Subject: [PATCH 051/140] Add fargate example for demo purposes --- .../hello-cdk-fargate/index.ts | 51 +++++++++++++++++++ examples/cdk-examples-typescript/package.json | 1 + 2 files changed, 52 insertions(+) create mode 100644 examples/cdk-examples-typescript/hello-cdk-fargate/index.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts new file mode 100644 index 0000000000000..3561b026fb7b6 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts @@ -0,0 +1,51 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import ecs = require('@aws-cdk/aws-ecs'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); + +class BonjourFargate extends cdk.Stack { + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + const vpc = new ec2.VpcNetwork(this, 'VPC'); + const cluster = new ecs.FargateCluster(this, 'Cluster', { + vpc + }); + + const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { + cpu: '512', + memoryMiB: '2GB' + }); + + taskDefinition.addContainer('WebApp', { + // image: new ecs.ImageFromSource('./my-webapp-source'), + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + // portMappings: [{ containerPort: 8080 }], + }); + + const service = new ecs.FargateService(this, 'Service', { + cluster, + taskDefinition + }); + + const loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LB', { + vpc, + internetFacing: true, + }); + + const listener = loadBalancer.addListener('Listener', { + port: 80, + open: true, + }); + + listener.addTargets('DefaultTargets', { + targets: [service], + protocol: elbv2.ApplicationProtocol.Http + }); + } +} + +const app = new cdk.App(process.argv); + +new BonjourFargate(app, 'Bonjour'); + +process.stdout.write(app.run()); diff --git a/examples/cdk-examples-typescript/package.json b/examples/cdk-examples-typescript/package.json index c3b2eae3dc1c4..79b2fd677a62a 100644 --- a/examples/cdk-examples-typescript/package.json +++ b/examples/cdk-examples-typescript/package.json @@ -30,6 +30,7 @@ "@aws-cdk/aws-ec2": "^0.10.0", "@aws-cdk/aws-ecs": "^0.10.0", "@aws-cdk/aws-elasticloadbalancing": "^0.10.0", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.10.0", "@aws-cdk/aws-iam": "^0.10.0", "@aws-cdk/aws-lambda": "^0.10.0", "@aws-cdk/aws-neptune": "^0.10.0", From 566aac3e5d362c6696b00683fffbeed43ba16ae3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 3 Oct 2018 12:24:15 +0200 Subject: [PATCH 052/140] Remove Service Role, fix DesiredCount default, make task memory definition work --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 26 ++----------------- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 1 - .../lib/fargate/fargate-task-definition.ts | 2 +- .../test/fargate/integ.lb-bridge-nw.ts | 17 ++++++------ 4 files changed, 12 insertions(+), 34 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index c06e03cfd0d61..72599bad8b7ee 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -1,6 +1,5 @@ import ec2 = require('@aws-cdk/aws-ec2'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); -import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; @@ -38,13 +37,6 @@ export interface BaseServiceProps { */ minimumHealthyPercent?: number; - /** - * Role used by ECS agent to register containers with the Load Balancer - * - * @default A role will be created for you - */ - role?: iam.Role; - /** * Time after startup to ignore unhealthy load balancer checks. * @@ -71,23 +63,20 @@ export abstract class BaseService extends cdk.Construct protected networkConfiguration?: cloudformation.ServiceResource.NetworkConfigurationProperty; protected readonly abstract taskDef: BaseTaskDefinition; protected _securityGroup?: ec2.SecurityGroupRef; - private role?: iam.Role; private readonly resource: cloudformation.ServiceResource; constructor(parent: cdk.Construct, name: string, props: BaseServiceProps, additionalProps: any) { super(parent, name); - this.role = props.role; - this.resource = new cloudformation.ServiceResource(this, "Service", { - desiredCount: props.desiredCount, + desiredCount: props.desiredCount || 1, serviceName: props.serviceName, loadBalancers: new cdk.Token(() => this.loadBalancers), deploymentConfiguration: { maximumPercent: props.maximumPercent, minimumHealthyPercent: props.minimumHealthyPercent }, - role: new cdk.Token(() => this.role && this.role.roleArn), + /* role: never specified, supplanted by Service Linked Role */ networkConfiguration: this.networkConfiguration, platformVersion: props.platformVersion, ...additionalProps @@ -116,16 +105,6 @@ export abstract class BaseService extends cdk.Construct return this._securityGroup!; } - protected createLoadBalancerRole() { - if (!this.role) { - this.role = new iam.Role(this, 'Role', { - assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), - }); - this.role.attachManagedPolicy(new iam.AwsManagedPolicy('service-role/AmazonEC2ContainerServiceRole').policyArn); - this.resource.addDependency(this.role); - } - } - // tslint:disable-next-line:max-line-length protected configureAwsVpcNetworking(vpc: ec2.VpcNetworkRef, assignPublicIp?: boolean, vpcPlacement?: ec2.VpcPlacementStrategy, securityGroup?: ec2.SecurityGroupRef) { if (vpcPlacement === undefined) { @@ -156,7 +135,6 @@ export abstract class BaseService extends cdk.Construct containerName: this.taskDef.defaultContainer!.id, containerPort: this.instancePort, }); - this.createLoadBalancerRole(); return { targetType: elbv2.TargetType.Ip }; } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index f9f9309558212..1808ab1f04590 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -179,7 +179,6 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { containerName: this.taskDefinition.defaultContainer!.id, containerPort: this.taskDefinition.defaultContainer!.instancePort, }); - this.createLoadBalancerRole(); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index 7e445d1ad2efe..4dd95a84319a2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -42,7 +42,7 @@ export class FargateTaskDefinition extends BaseTaskDefinition { constructor(parent: cdk.Construct, name: string, props: FargateTaskDefinitionProps = {}) { super(parent, name, props, { cpu: props.cpu || '256', - memoryMiB: props.memoryMiB || '512', + memory: props.memoryMiB || '512', networkMode: NetworkMode.AwsVpc, requiresCompatibilities: [Compatibilities.Fargate] }); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts index 76be274f5d3f5..1395a36192efb 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts @@ -1,21 +1,21 @@ import ec2 = require('@aws-cdk/aws-ec2'); -// import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); -// import ecs = require('../../lib'); +import ecs = require('../../lib'); const app = new cdk.App(process.argv); const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -Array.isArray(vpc); -/* const cluster = new ecs.FargateCluster(stack, 'EcsCluster', { vpc }); -const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef'); +const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { + memoryMiB: '1GB', + cpu: '512' +}); taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - cpu: 256, - memoryMiB: 512, + memoryLimitMiB: 1024, }); const service = new ecs.FargateService(stack, "Service", { @@ -29,6 +29,7 @@ listener.addTargets('ECS', { port: 80, targets: [service] }); -*/ + +new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName }); process.stdout.write(app.run()); \ No newline at end of file From 48cd8d4591a7934cc79947c42abc07a7e7eb99c3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 3 Oct 2018 13:26:21 +0200 Subject: [PATCH 053/140] Starting on port mapping defaults --- .../aws-ecs/lib/container-definition.ts | 36 +++++++++++++++---- .../test/fargate/integ.lb-bridge-nw.ts | 6 +++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 3342499326f12..b683fc7fde655 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -230,7 +230,10 @@ export class ContainerDefinition extends cdk.Construct { * Return the instance port that the container will be listening on */ public get instancePort(): number { - return 0; + if (this.portMappings.length === 0) { + throw new Error(`Container ${this.id} hasn't defined any ports`); + } + return this.portMappings[0].hostPort || this.portMappings[0].containerPort; } public renderContainerDefinition(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { @@ -391,11 +394,32 @@ function renderUlimit(ulimit: Ulimit): cloudformation.TaskDefinitionResource.Uli }; } -// TODO: add default? +/** + * Map a host port to a container port + */ export interface PortMapping { - containerPort?: number, - hostPort?: number, - protocol: Protocol + /** + * Port inside the container + */ + containerPort: number; + + /** + * Port on the host + * + * In AwsVpc or Host networking mode, leave this out or set it to the + * same value as containerPort. + * + * In Bridge networking mode, leave this out or set it to non-reserved + * non-ephemeral port. + */ + hostPort?: number; + + /** + * Protocol + * + * @default Tcp + */ + protocol?: Protocol } export enum Protocol { @@ -407,7 +431,7 @@ function renderPortMapping(pm: PortMapping): cloudformation.TaskDefinitionResour return { containerPort: pm.containerPort, hostPort: pm.hostPort, - protocol: pm.protocol, + protocol: pm.protocol || Protocol.Tcp, }; } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts index 1395a36192efb..542975396d13b 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts @@ -13,10 +13,14 @@ const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { memoryMiB: '1GB', cpu: '512' }); -taskDefinition.addContainer('web', { +const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 1024, }); +container.addPortMappings({ + containerPort: 8080, + protocol: ecs.Protocol.Tcp +}); const service = new ecs.FargateService(stack, "Service", { cluster, From e311293ec2c1ebe3077c07427181ae2a0d6b5688 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 3 Oct 2018 14:56:14 +0200 Subject: [PATCH 054/140] Add validation on port mappings with tests --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- .../aws-ecs/lib/base/base-task-definition.ts | 3 +- .../aws-ecs/lib/container-definition.ts | 15 +- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 2 +- .../aws-ecs/test/test.container-definition.ts | 131 ++++++++++++++++++ 5 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/test.container-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 72599bad8b7ee..3b5778edded73 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -145,7 +145,7 @@ export abstract class BaseService extends cdk.Construct * Returns 0 if the networking mode implies dynamic port allocation. */ private get instancePort() { - return this.taskDef.networkMode === NetworkMode.Bridge ? 0 : this.taskDef.defaultContainer!.instancePort; + return this.taskDef.networkMode === NetworkMode.Bridge ? 0 : this.taskDef.defaultContainer!.ingressPort; } } diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index 7b4159a226048..4c71d62a2a0d5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -89,7 +89,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { * Add a container to this task */ public addContainer(id: string, props: ContainerDefinitionProps) { - const container = new ContainerDefinition(this, id, props); + const container = new ContainerDefinition(this, id, this, props); this.containers.push(container); if (container.usesEcrImages) { this.generateExecutionRole(); @@ -97,6 +97,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { if (this.defaultContainer === undefined && container.essential) { this.defaultContainer = container; } + return container; } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index b683fc7fde655..bdcf18cd59f68 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/cdk'); +import { BaseTaskDefinition, NetworkMode } from './base/base-task-definition'; import { ContainerImage } from './container-image'; import { cloudformation } from './ecs.generated'; import { LinuxParameters } from './linux-parameters'; @@ -183,11 +184,14 @@ export class ContainerDefinition extends cdk.Construct { private readonly links = new Array(); + private readonly taskDefinition: BaseTaskDefinition; + private _usesEcrImages: boolean = false; - constructor(parent: cdk.Construct, id: string, private readonly props: ContainerDefinitionProps) { + constructor(parent: cdk.Construct, id: string, taskDefinition: BaseTaskDefinition, private readonly props: ContainerDefinitionProps) { super(parent, id); this.essential = props.essential !== undefined ? props.essential : true; + this.taskDefinition = taskDefinition; props.image.bind(this); } @@ -204,6 +208,13 @@ export class ContainerDefinition extends cdk.Construct { } public addPortMappings(...portMappings: PortMapping[]) { + for (const pm of portMappings) { + if (this.taskDefinition.networkMode === NetworkMode.AwsVpc || this.taskDefinition.networkMode === NetworkMode.Host) { + if (pm.containerPort !== pm.hostPort && pm.hostPort !== undefined) { + throw new Error(`Host port ${pm.hostPort} does not match container port ${pm.containerPort}.`); + } + } + } this.portMappings.push(...portMappings); } @@ -229,7 +240,7 @@ export class ContainerDefinition extends cdk.Construct { /** * Return the instance port that the container will be listening on */ - public get instancePort(): number { + public get ingressPort(): number { if (this.portMappings.length === 0) { throw new Error(`Container ${this.id} hasn't defined any ports`); } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 1808ab1f04590..f389d4b0b45a4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -177,7 +177,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { this.loadBalancers.push({ loadBalancerName: loadBalancer.loadBalancerName, containerName: this.taskDefinition.defaultContainer!.id, - containerPort: this.taskDefinition.defaultContainer!.instancePort, + containerPort: this.taskDefinition.defaultContainer!.ingressPort, }); } } diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts new file mode 100644 index 0000000000000..ef88fecaa9596 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -0,0 +1,131 @@ +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../lib'); + +export = { + "When creating a Task Definition": { + // Validating portMapping inputs + "With network mode AwsVpc": { + "Host port should be the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + // WHEN + test.throws(() => { + container.addPortMappings({ + containerPort: 8080, + hostPort: 8081 + }); + }); + // THEN + test.done(); + }, + + "Host port can be empty "(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + + // THEN no excpetion raised + test.done(); + }, + }, + "With network mode Host ": { + "Host port should be the same as container port"(test: Test) { + test.done(); + }, + "Host port can be empty "(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + + // THEN no excpetion raised + test.done(); + }, + }, + "With network mode Bridge": { + "Host port should not be lower than 1024"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + test.throws(() => { + container.addPortMappings({ + containerPort: 8080, + hostPort: 1, + }); + }); + + // THEN + test.done(); + }, + }, + + // "With health check": { + // "healthCheck.command is a single string"(test: Test) { + // const stack = new cdk.Stack(); + // const taskDefinition = new TaskDefinition(stack, 'TaskDef'); + // const containerDefinition = taskDefinition.ContainerDefinition[0]; + // test.deepEqual(resolve(vpc.vpcId), {Ref: 'TheVPC92636AB0' } ); + // test.done(); + // }, + // } + }, + "Ingress Port": { + "With network mode AwsVpc": { + "Ingress port should be the same as container port"(test: Test) { + test.done(); + }, + }, + "With network mode Host ": { + "Ingress port should be the same as container port"(test: Test) { + test.done(); + }, + }, + "With network mode Bridge": { + "Ingress port should be the same as host port if supplied"(test: Test) { + test.done(); + }, + "Ingress port should be the 0 if not supplied"(test: Test) { + test.done(); + }, + }, + }, +}; From 4ffed0922a2b8bb7e9f1f0be45b700b343af35b9 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 3 Oct 2018 15:19:28 +0200 Subject: [PATCH 055/140] Implement ingressPort method --- .../aws-ecs/lib/container-definition.ts | 9 +- .../aws-ecs/test/test.container-definition.ts | 90 +++++++++++++++++-- 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index bdcf18cd59f68..ed1af1f3727dc 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -244,7 +244,14 @@ export class ContainerDefinition extends cdk.Construct { if (this.portMappings.length === 0) { throw new Error(`Container ${this.id} hasn't defined any ports`); } - return this.portMappings[0].hostPort || this.portMappings[0].containerPort; + const defaultPortMapping = this.portMappings[0]; + if (defaultPortMapping.hostPort !== undefined) { + return defaultPortMapping.hostPort; + } + if (this.taskDefinition.networkMode === NetworkMode.Bridge) { + return 0; + } + return defaultPortMapping.containerPort; } public renderContainerDefinition(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index ef88fecaa9596..06476589066a4 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -17,14 +17,13 @@ export = { image: ecs.DockerHub.image("/aws/aws-example-app"), memoryLimitMiB: 2048, }); - // WHEN + // THEN test.throws(() => { container.addPortMappings({ containerPort: 8080, hostPort: 8081 }); }); - // THEN test.done(); }, @@ -68,7 +67,7 @@ export = { containerPort: 8080, }); - // THEN no excpetion raised + // THEN no exception raised test.done(); }, }, @@ -85,15 +84,13 @@ export = { memoryLimitMiB: 2048, }); - // WHEN + // THEN test.throws(() => { container.addPortMappings({ containerPort: 8080, hostPort: 1, }); }); - - // THEN test.done(); }, }, @@ -111,19 +108,100 @@ export = { "Ingress Port": { "With network mode AwsVpc": { "Ingress port should be the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + const actual = container.ingressPort; + + // THEN + const expected = 8080; + test.equal(actual, expected, "Ingress port should be the same as container port"); test.done(); }, }, "With network mode Host ": { "Ingress port should be the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Host, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + const actual = container.ingressPort; + + // THEN + const expected = 8080; + test.equal(actual, expected); test.done(); }, }, "With network mode Bridge": { "Ingress port should be the same as host port if supplied"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Bridge, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8081, + hostPort: 8080, + }); + const actual = container.ingressPort; + + // THEN + const expected = 8080; + test.equal(actual, expected); test.done(); }, "Ingress port should be the 0 if not supplied"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Bridge, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8081, + }); + const actual = container.ingressPort; + + // THEN + const expected = 0; + test.equal(actual, expected); test.done(); }, }, From 97d3db3e73c31ae9bbd5a7a1a80dd5ae2bc2359d Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 4 Oct 2018 11:09:12 +0200 Subject: [PATCH 056/140] Make ALB listener ACTUALLY default to 'true' --- .../aws-elasticloadbalancingv2/lib/alb/application-listener.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index e488c7ac7901d..18f5f8d803542 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -134,7 +134,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis (props.defaultTargetGroups || []).forEach(this.addDefaultTargetGroup.bind(this)); - if (props.open) { + if (props.open !== false) { this.connections.allowDefaultPortFrom(new ec2.AnyIPv4(), `Allow from anyone on port ${port}`); } } From 4f9b54818fa07c62a673309887f74329f4b8f27d Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 11:10:33 +0200 Subject: [PATCH 057/140] Add dependency on listener This ensures that the dependency resolution by CFN works properly when attaching a load balancer to an ECS service --- .../lib/alb/application-listener.ts | 3 ++- .../lib/alb/application-target-group.ts | 7 ++++++- .../lib/nlb/network-listener.ts | 4 +++- .../lib/nlb/network-target-group.ts | 14 ++++++++++++++ .../lib/shared/base-listener.ts | 4 +++- .../lib/shared/base-target-group.ts | 19 ++++++++++++++++++- .../lib/shared/util.ts | 13 +++++++++++++ 7 files changed, 59 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index e488c7ac7901d..599b5bc0c7309 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -258,7 +258,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis /** * Properties to reference an existing listener */ -export interface IApplicationListener extends ec2.IConnectable { +export interface IApplicationListener extends ec2.IConnectable, cdk.IDependable { /** * ARN of the listener */ @@ -319,6 +319,7 @@ export interface ApplicationListenerRefProps { } class ImportedApplicationListener extends cdk.Construct implements IApplicationListener { + public readonly dependencyElements: cdk.IDependable[] = []; public readonly connections: ec2.Connections; /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts index b6f21689df72f..2454f856af895 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts @@ -3,7 +3,7 @@ import cdk = require('@aws-cdk/cdk'); import { BaseTargetGroup, BaseTargetGroupProps, ITargetGroup, LoadBalancerTargetProps, TargetGroupRefProps } from '../shared/base-target-group'; import { ApplicationProtocol } from '../shared/enums'; import { BaseImportedTargetGroup } from '../shared/imported'; -import { determineProtocolAndPort } from '../shared/util'; +import { determineProtocolAndPort, LazyDependable } from '../shared/util'; import { IApplicationListener } from './application-listener'; /** @@ -144,6 +144,7 @@ export class ApplicationTargetGroup extends BaseTargetGroup { listener.registerConnectable(member.connectable, member.portRange); } this.listeners.push(listener); + this.dependableListeners.push(listener); } } @@ -181,6 +182,10 @@ class ImportedApplicationTargetGroup extends BaseImportedTargetGroup implements public registerListener(_listener: IApplicationListener) { // Nothing to do, we know nothing of our members } + + public listenerDependency(): cdk.IDependable { + return new LazyDependable([]); + } } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts index e72e8f1c951df..99c89342fa7a2 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts @@ -113,7 +113,7 @@ export class NetworkListener extends BaseListener implements INetworkListener { /** * Properties to reference an existing listener */ -export interface INetworkListener { +export interface INetworkListener extends cdk.IDependable { /** * ARN of the listener */ @@ -134,6 +134,8 @@ export interface NetworkListenerRefProps { * An imported Network Listener */ class ImportedNetworkListener extends cdk.Construct implements INetworkListener { + public readonly dependencyElements: cdk.IDependable[] = []; + /** * ARN of the listener */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts index 5dc2253d84f35..4cb59fbdfad2e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts @@ -2,6 +2,8 @@ import cdk = require('@aws-cdk/cdk'); import { BaseTargetGroup, BaseTargetGroupProps, ITargetGroup, LoadBalancerTargetProps, TargetGroupRefProps } from '../shared/base-target-group'; import { Protocol } from '../shared/enums'; import { BaseImportedTargetGroup } from '../shared/imported'; +import { LazyDependable } from '../shared/util'; +import { INetworkListener } from './network-listener'; /** * Properties for a new Network Target Group @@ -62,6 +64,15 @@ export class NetworkTargetGroup extends BaseTargetGroup { this.addLoadBalancerTarget(result); } } + + /** + * Register a listener that is load balancing to this target group. + * + * Don't call this directly. It will be called by listeners. + */ + public registerListener(listener: INetworkListener) { + this.dependableListeners.push(listener); + } } /** @@ -75,6 +86,9 @@ export interface INetworkTargetGroup extends ITargetGroup { * An imported network target group */ class ImportedNetworkTargetGroup extends BaseImportedTargetGroup implements INetworkTargetGroup { + public listenerDependency(): cdk.IDependable { + return new LazyDependable([]); + } } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index 6bc12566cb6f4..af212853acea7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -5,7 +5,8 @@ import { ITargetGroup } from './base-target-group'; /** * Base class for listeners */ -export abstract class BaseListener extends cdk.Construct { +export abstract class BaseListener extends cdk.Construct implements cdk.IDependable { + public readonly dependencyElements: cdk.IDependable[]; public readonly listenerArn: string; private readonly defaultActions: any[] = []; @@ -17,6 +18,7 @@ export abstract class BaseListener extends cdk.Construct { defaultActions: new cdk.Token(() => this.defaultActions), }); + this.dependencyElements = [resource]; this.listenerArn = resource.ref; } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 356456de0ab26..280b2ab25bd82 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -2,7 +2,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation } from '../elasticloadbalancingv2.generated'; import { Protocol, TargetType } from './enums'; -import { Attributes, renderAttributes } from './util'; +import { Attributes, LazyDependable, renderAttributes } from './util'; /** * Basic properties of both Application and Network Target Groups @@ -145,6 +145,11 @@ export abstract class BaseTargetGroup extends cdk.Construct implements ITargetGr */ protected readonly defaultPort: string; + /** + * List of listeners routing to this target group + */ + protected readonly dependableListeners = new Array(); + /** * Attributes of this target group */ @@ -234,6 +239,13 @@ export abstract class BaseTargetGroup extends cdk.Construct implements ITargetGr this.resource.addDependency(...other); } + /** + * Return an object to depend on the listeners added to this target group + */ + public listenerDependency(): cdk.IDependable { + return new LazyDependable(this.dependableListeners); + } + /** * Register the given load balancing target as part of this group */ @@ -272,6 +284,11 @@ export interface ITargetGroup { * ARN of the target group */ readonly targetGroupArn: string; + + /** + * Return an object to depend on the listeners added to this target group + */ + listenerDependency(): cdk.IDependable; } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts index d992d156a3b2f..808a8f0e2c33e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts @@ -1,3 +1,4 @@ +import cdk = require('@aws-cdk/cdk'); import { ApplicationProtocol } from "./enums"; export type Attributes = {[key: string]: string | undefined}; @@ -67,3 +68,15 @@ export function determineProtocolAndPort(protocol: ApplicationProtocol | undefin export function ifUndefined(x: T | undefined, def: T) { return x !== undefined ? x : def; } + +/** + * Allow lazy evaluation of a list of dependables + */ +export class LazyDependable implements cdk.IDependable { + constructor(private readonly depList: cdk.IDependable[]) { + } + + public get dependencyElements(): cdk.IDependable[] { + return this.depList; + } +} \ No newline at end of file From e8aa186b0b8beaab1ae1dfeffbab9d75faca1c77 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 11:51:43 +0200 Subject: [PATCH 058/140] Integ test for Fargate service Notes: - Loadbalancer needs to be specified to be "internetFacing" - Cannot modify LoadBalancer on service once service has been created; can get around this by modifying ID of LB - Listener must be set to open --- packages/@aws-cdk/aws-ecs/test/fargate/cdk.json | 9 +++++++++ .../{integ.lb-bridge-nw.ts => integ.lb-awsvpc-nw.ts} | 9 +++++---- 2 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/cdk.json rename packages/@aws-cdk/aws-ecs/test/fargate/{integ.lb-bridge-nw.ts => integ.lb-awsvpc-nw.ts} (78%) diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json new file mode 100644 index 0000000000000..64fd3b2dafe6e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json @@ -0,0 +1,9 @@ +{ + "context": { + "availability-zones:794715269151:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" + ] + } +} diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts similarity index 78% rename from packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts rename to packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index 542975396d13b..c9db123d8a7e2 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -7,6 +7,7 @@ const app = new cdk.App(process.argv); const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + const cluster = new ecs.FargateCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { @@ -18,7 +19,7 @@ const container = taskDefinition.addContainer('web', { memoryLimitMiB: 1024, }); container.addPortMappings({ - containerPort: 8080, + containerPort: 80, protocol: ecs.Protocol.Tcp }); @@ -27,13 +28,13 @@ const service = new ecs.FargateService(stack, "Service", { taskDefinition, }); -const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); -const listener = lb.addListener('PublicListener', { port: 80 }); +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); +const listener = lb.addListener('PublicListener', { port: 80, open: true }); listener.addTargets('ECS', { port: 80, targets: [service] }); -new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName }); +new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); process.stdout.write(app.run()); \ No newline at end of file From e06a3ec08ccd2fcb2fec04e5a3f70c70df0618d1 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 2 Oct 2018 16:57:14 +0200 Subject: [PATCH 059/140] Add VolumesFrom to ContainerDefinition --- .../hello-cdk-ecs/index.ts | 4 ++++ .../aws-ecs/lib/container-definition.ts | 24 +++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index ad387476873e2..97ba362de1b9c 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -82,6 +82,10 @@ class BonjourECS extends cdk.Stack { readOnly: true, }); + container.addVolumesFrom({ + sourceContainer: 'web', + readOnly: true, + }); new ecs.EcsService(this, "EcsService", { cluster, diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index c73c198fa6d49..3342499326f12 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -175,6 +175,8 @@ export class ContainerDefinition extends cdk.Construct { public readonly portMappings = new Array(); + public readonly volumesFrom = new Array(); + public readonly ulimits = new Array(); public readonly essential: boolean; @@ -209,6 +211,10 @@ export class ContainerDefinition extends cdk.Construct { this.ulimits.push(...ulimits); } + public addVolumesFrom(...volumesFrom: VolumeFrom[]) { + this.volumesFrom.push(...volumesFrom); + } + /** * Mark this ContainerDefinition as using an ECR image */ @@ -250,7 +256,7 @@ export class ContainerDefinition extends cdk.Construct { repositoryCredentials: undefined, // FIXME ulimits: this.ulimits.map(renderUlimit), user: this.props.user, - volumesFrom: [], // FIXME + volumesFrom: this.volumesFrom.map(renderVolumeFrom), workingDirectory: this.props.workingDirectory, logConfiguration: this.props.logging && this.props.logging.renderLogDriver(), environment: this.props.environment && renderKV(this.props.environment, 'name', 'value'), @@ -310,10 +316,6 @@ export interface HealthCheck { timeout?: number; } -// mountPoints?: mountPoint[]; -// portMappings?: portMapping[]; -// volumesFrom?: volumeFrom[]; - function renderKV(env: {[key: string]: string}, keyName: string, valueName: string): any { const ret = []; for (const [key, value] of Object.entries(env)) { @@ -422,3 +424,15 @@ function renderMountPoint(mp: MountPoint): cloudformation.TaskDefinitionResource sourceVolume: mp.sourceVolume, }; } + +export interface VolumeFrom { + sourceContainer: string, + readOnly: boolean, +} + +function renderVolumeFrom(vf: VolumeFrom): cloudformation.TaskDefinitionResource.VolumeFromProperty { + return { + sourceContainer: vf.sourceContainer, + readOnly: vf.readOnly, + }; +} From 95b8d848b0b7e2501fae4a7e4157a0c9899a03ff Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 3 Oct 2018 12:06:06 +0200 Subject: [PATCH 060/140] Add fargate example for demo purposes --- .../hello-cdk-fargate/index.ts | 51 +++++++++++++++++++ examples/cdk-examples-typescript/package.json | 1 + 2 files changed, 52 insertions(+) create mode 100644 examples/cdk-examples-typescript/hello-cdk-fargate/index.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts new file mode 100644 index 0000000000000..3561b026fb7b6 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts @@ -0,0 +1,51 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import ecs = require('@aws-cdk/aws-ecs'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); + +class BonjourFargate extends cdk.Stack { + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + const vpc = new ec2.VpcNetwork(this, 'VPC'); + const cluster = new ecs.FargateCluster(this, 'Cluster', { + vpc + }); + + const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { + cpu: '512', + memoryMiB: '2GB' + }); + + taskDefinition.addContainer('WebApp', { + // image: new ecs.ImageFromSource('./my-webapp-source'), + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + // portMappings: [{ containerPort: 8080 }], + }); + + const service = new ecs.FargateService(this, 'Service', { + cluster, + taskDefinition + }); + + const loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LB', { + vpc, + internetFacing: true, + }); + + const listener = loadBalancer.addListener('Listener', { + port: 80, + open: true, + }); + + listener.addTargets('DefaultTargets', { + targets: [service], + protocol: elbv2.ApplicationProtocol.Http + }); + } +} + +const app = new cdk.App(process.argv); + +new BonjourFargate(app, 'Bonjour'); + +process.stdout.write(app.run()); diff --git a/examples/cdk-examples-typescript/package.json b/examples/cdk-examples-typescript/package.json index c3b2eae3dc1c4..79b2fd677a62a 100644 --- a/examples/cdk-examples-typescript/package.json +++ b/examples/cdk-examples-typescript/package.json @@ -30,6 +30,7 @@ "@aws-cdk/aws-ec2": "^0.10.0", "@aws-cdk/aws-ecs": "^0.10.0", "@aws-cdk/aws-elasticloadbalancing": "^0.10.0", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.10.0", "@aws-cdk/aws-iam": "^0.10.0", "@aws-cdk/aws-lambda": "^0.10.0", "@aws-cdk/aws-neptune": "^0.10.0", From 55957074c1f694a8c43128f5d348a929576d005e Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 3 Oct 2018 13:26:21 +0200 Subject: [PATCH 061/140] Starting on port mapping defaults --- .../aws-ecs/lib/container-definition.ts | 36 +++++++++++++++---- .../test/fargate/integ.lb-bridge-nw.ts | 6 +++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 3342499326f12..b683fc7fde655 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -230,7 +230,10 @@ export class ContainerDefinition extends cdk.Construct { * Return the instance port that the container will be listening on */ public get instancePort(): number { - return 0; + if (this.portMappings.length === 0) { + throw new Error(`Container ${this.id} hasn't defined any ports`); + } + return this.portMappings[0].hostPort || this.portMappings[0].containerPort; } public renderContainerDefinition(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { @@ -391,11 +394,32 @@ function renderUlimit(ulimit: Ulimit): cloudformation.TaskDefinitionResource.Uli }; } -// TODO: add default? +/** + * Map a host port to a container port + */ export interface PortMapping { - containerPort?: number, - hostPort?: number, - protocol: Protocol + /** + * Port inside the container + */ + containerPort: number; + + /** + * Port on the host + * + * In AwsVpc or Host networking mode, leave this out or set it to the + * same value as containerPort. + * + * In Bridge networking mode, leave this out or set it to non-reserved + * non-ephemeral port. + */ + hostPort?: number; + + /** + * Protocol + * + * @default Tcp + */ + protocol?: Protocol } export enum Protocol { @@ -407,7 +431,7 @@ function renderPortMapping(pm: PortMapping): cloudformation.TaskDefinitionResour return { containerPort: pm.containerPort, hostPort: pm.hostPort, - protocol: pm.protocol, + protocol: pm.protocol || Protocol.Tcp, }; } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts index 1395a36192efb..542975396d13b 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts @@ -13,10 +13,14 @@ const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { memoryMiB: '1GB', cpu: '512' }); -taskDefinition.addContainer('web', { +const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 1024, }); +container.addPortMappings({ + containerPort: 8080, + protocol: ecs.Protocol.Tcp +}); const service = new ecs.FargateService(stack, "Service", { cluster, From a1b78f471a62d739498d9fbec1caed8cd42c7bbf Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 4 Oct 2018 11:09:12 +0200 Subject: [PATCH 062/140] Make ALB listener ACTUALLY default to 'true' --- .../aws-elasticloadbalancingv2/lib/alb/application-listener.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index e488c7ac7901d..18f5f8d803542 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -134,7 +134,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis (props.defaultTargetGroups || []).forEach(this.addDefaultTargetGroup.bind(this)); - if (props.open) { + if (props.open !== false) { this.connections.allowDefaultPortFrom(new ec2.AnyIPv4(), `Allow from anyone on port ${port}`); } } From 6f08406df67be77bb00b50ef94fa85361a8b0041 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 3 Oct 2018 14:56:14 +0200 Subject: [PATCH 063/140] Add validation on port mappings with tests --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- .../aws-ecs/lib/base/base-task-definition.ts | 3 +- .../aws-ecs/lib/container-definition.ts | 15 +- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 2 +- .../aws-ecs/test/test.container-definition.ts | 131 ++++++++++++++++++ 5 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/test.container-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 72599bad8b7ee..3b5778edded73 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -145,7 +145,7 @@ export abstract class BaseService extends cdk.Construct * Returns 0 if the networking mode implies dynamic port allocation. */ private get instancePort() { - return this.taskDef.networkMode === NetworkMode.Bridge ? 0 : this.taskDef.defaultContainer!.instancePort; + return this.taskDef.networkMode === NetworkMode.Bridge ? 0 : this.taskDef.defaultContainer!.ingressPort; } } diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index 7b4159a226048..4c71d62a2a0d5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -89,7 +89,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { * Add a container to this task */ public addContainer(id: string, props: ContainerDefinitionProps) { - const container = new ContainerDefinition(this, id, props); + const container = new ContainerDefinition(this, id, this, props); this.containers.push(container); if (container.usesEcrImages) { this.generateExecutionRole(); @@ -97,6 +97,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { if (this.defaultContainer === undefined && container.essential) { this.defaultContainer = container; } + return container; } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index b683fc7fde655..bdcf18cd59f68 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/cdk'); +import { BaseTaskDefinition, NetworkMode } from './base/base-task-definition'; import { ContainerImage } from './container-image'; import { cloudformation } from './ecs.generated'; import { LinuxParameters } from './linux-parameters'; @@ -183,11 +184,14 @@ export class ContainerDefinition extends cdk.Construct { private readonly links = new Array(); + private readonly taskDefinition: BaseTaskDefinition; + private _usesEcrImages: boolean = false; - constructor(parent: cdk.Construct, id: string, private readonly props: ContainerDefinitionProps) { + constructor(parent: cdk.Construct, id: string, taskDefinition: BaseTaskDefinition, private readonly props: ContainerDefinitionProps) { super(parent, id); this.essential = props.essential !== undefined ? props.essential : true; + this.taskDefinition = taskDefinition; props.image.bind(this); } @@ -204,6 +208,13 @@ export class ContainerDefinition extends cdk.Construct { } public addPortMappings(...portMappings: PortMapping[]) { + for (const pm of portMappings) { + if (this.taskDefinition.networkMode === NetworkMode.AwsVpc || this.taskDefinition.networkMode === NetworkMode.Host) { + if (pm.containerPort !== pm.hostPort && pm.hostPort !== undefined) { + throw new Error(`Host port ${pm.hostPort} does not match container port ${pm.containerPort}.`); + } + } + } this.portMappings.push(...portMappings); } @@ -229,7 +240,7 @@ export class ContainerDefinition extends cdk.Construct { /** * Return the instance port that the container will be listening on */ - public get instancePort(): number { + public get ingressPort(): number { if (this.portMappings.length === 0) { throw new Error(`Container ${this.id} hasn't defined any ports`); } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 1808ab1f04590..f389d4b0b45a4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -177,7 +177,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { this.loadBalancers.push({ loadBalancerName: loadBalancer.loadBalancerName, containerName: this.taskDefinition.defaultContainer!.id, - containerPort: this.taskDefinition.defaultContainer!.instancePort, + containerPort: this.taskDefinition.defaultContainer!.ingressPort, }); } } diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts new file mode 100644 index 0000000000000..ef88fecaa9596 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -0,0 +1,131 @@ +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../lib'); + +export = { + "When creating a Task Definition": { + // Validating portMapping inputs + "With network mode AwsVpc": { + "Host port should be the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + // WHEN + test.throws(() => { + container.addPortMappings({ + containerPort: 8080, + hostPort: 8081 + }); + }); + // THEN + test.done(); + }, + + "Host port can be empty "(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + + // THEN no excpetion raised + test.done(); + }, + }, + "With network mode Host ": { + "Host port should be the same as container port"(test: Test) { + test.done(); + }, + "Host port can be empty "(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + + // THEN no excpetion raised + test.done(); + }, + }, + "With network mode Bridge": { + "Host port should not be lower than 1024"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + test.throws(() => { + container.addPortMappings({ + containerPort: 8080, + hostPort: 1, + }); + }); + + // THEN + test.done(); + }, + }, + + // "With health check": { + // "healthCheck.command is a single string"(test: Test) { + // const stack = new cdk.Stack(); + // const taskDefinition = new TaskDefinition(stack, 'TaskDef'); + // const containerDefinition = taskDefinition.ContainerDefinition[0]; + // test.deepEqual(resolve(vpc.vpcId), {Ref: 'TheVPC92636AB0' } ); + // test.done(); + // }, + // } + }, + "Ingress Port": { + "With network mode AwsVpc": { + "Ingress port should be the same as container port"(test: Test) { + test.done(); + }, + }, + "With network mode Host ": { + "Ingress port should be the same as container port"(test: Test) { + test.done(); + }, + }, + "With network mode Bridge": { + "Ingress port should be the same as host port if supplied"(test: Test) { + test.done(); + }, + "Ingress port should be the 0 if not supplied"(test: Test) { + test.done(); + }, + }, + }, +}; From 531236286b9adf83c5960e906a9e70e0681da1c4 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 3 Oct 2018 15:19:28 +0200 Subject: [PATCH 064/140] Implement ingressPort method --- .../aws-ecs/lib/container-definition.ts | 9 +- .../aws-ecs/test/test.container-definition.ts | 90 +++++++++++++++++-- 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index bdcf18cd59f68..ed1af1f3727dc 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -244,7 +244,14 @@ export class ContainerDefinition extends cdk.Construct { if (this.portMappings.length === 0) { throw new Error(`Container ${this.id} hasn't defined any ports`); } - return this.portMappings[0].hostPort || this.portMappings[0].containerPort; + const defaultPortMapping = this.portMappings[0]; + if (defaultPortMapping.hostPort !== undefined) { + return defaultPortMapping.hostPort; + } + if (this.taskDefinition.networkMode === NetworkMode.Bridge) { + return 0; + } + return defaultPortMapping.containerPort; } public renderContainerDefinition(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index ef88fecaa9596..06476589066a4 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -17,14 +17,13 @@ export = { image: ecs.DockerHub.image("/aws/aws-example-app"), memoryLimitMiB: 2048, }); - // WHEN + // THEN test.throws(() => { container.addPortMappings({ containerPort: 8080, hostPort: 8081 }); }); - // THEN test.done(); }, @@ -68,7 +67,7 @@ export = { containerPort: 8080, }); - // THEN no excpetion raised + // THEN no exception raised test.done(); }, }, @@ -85,15 +84,13 @@ export = { memoryLimitMiB: 2048, }); - // WHEN + // THEN test.throws(() => { container.addPortMappings({ containerPort: 8080, hostPort: 1, }); }); - - // THEN test.done(); }, }, @@ -111,19 +108,100 @@ export = { "Ingress Port": { "With network mode AwsVpc": { "Ingress port should be the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AwsVpc, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + const actual = container.ingressPort; + + // THEN + const expected = 8080; + test.equal(actual, expected, "Ingress port should be the same as container port"); test.done(); }, }, "With network mode Host ": { "Ingress port should be the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Host, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + const actual = container.ingressPort; + + // THEN + const expected = 8080; + test.equal(actual, expected); test.done(); }, }, "With network mode Bridge": { "Ingress port should be the same as host port if supplied"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Bridge, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8081, + hostPort: 8080, + }); + const actual = container.ingressPort; + + // THEN + const expected = 8080; + test.equal(actual, expected); test.done(); }, "Ingress port should be the 0 if not supplied"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Bridge, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8081, + }); + const actual = container.ingressPort; + + // THEN + const expected = 0; + test.equal(actual, expected); test.done(); }, }, From 6f42026f22017b7f05daa57ae6084be0c2e5b597 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 11:10:33 +0200 Subject: [PATCH 065/140] Add dependency on listener This ensures that the dependency resolution by CFN works properly when attaching a load balancer to an ECS service --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 4 +++- .../lib/alb/application-listener.ts | 3 ++- .../lib/alb/application-target-group.ts | 7 ++++++- .../lib/nlb/network-listener.ts | 4 +++- .../lib/nlb/network-target-group.ts | 14 ++++++++++++++ .../lib/shared/base-listener.ts | 4 +++- .../lib/shared/base-target-group.ts | 19 ++++++++++++++++++- .../lib/shared/util.ts | 13 +++++++++++++ 8 files changed, 62 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 3b5778edded73..67d8cf58e23c3 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -136,6 +136,8 @@ export abstract class BaseService extends cdk.Construct containerPort: this.instancePort, }); + this.resource.addDependency(targetGroup.listenerDependency()); + return { targetType: elbv2.TargetType.Ip }; } @@ -185,4 +187,4 @@ export enum FargatePlatformVersion { * Based on Amazon Linux 2017.09. */ Version10 = '1.0.0', -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 18f5f8d803542..217db667db327 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -258,7 +258,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis /** * Properties to reference an existing listener */ -export interface IApplicationListener extends ec2.IConnectable { +export interface IApplicationListener extends ec2.IConnectable, cdk.IDependable { /** * ARN of the listener */ @@ -319,6 +319,7 @@ export interface ApplicationListenerRefProps { } class ImportedApplicationListener extends cdk.Construct implements IApplicationListener { + public readonly dependencyElements: cdk.IDependable[] = []; public readonly connections: ec2.Connections; /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts index b6f21689df72f..2454f856af895 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts @@ -3,7 +3,7 @@ import cdk = require('@aws-cdk/cdk'); import { BaseTargetGroup, BaseTargetGroupProps, ITargetGroup, LoadBalancerTargetProps, TargetGroupRefProps } from '../shared/base-target-group'; import { ApplicationProtocol } from '../shared/enums'; import { BaseImportedTargetGroup } from '../shared/imported'; -import { determineProtocolAndPort } from '../shared/util'; +import { determineProtocolAndPort, LazyDependable } from '../shared/util'; import { IApplicationListener } from './application-listener'; /** @@ -144,6 +144,7 @@ export class ApplicationTargetGroup extends BaseTargetGroup { listener.registerConnectable(member.connectable, member.portRange); } this.listeners.push(listener); + this.dependableListeners.push(listener); } } @@ -181,6 +182,10 @@ class ImportedApplicationTargetGroup extends BaseImportedTargetGroup implements public registerListener(_listener: IApplicationListener) { // Nothing to do, we know nothing of our members } + + public listenerDependency(): cdk.IDependable { + return new LazyDependable([]); + } } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts index e72e8f1c951df..99c89342fa7a2 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts @@ -113,7 +113,7 @@ export class NetworkListener extends BaseListener implements INetworkListener { /** * Properties to reference an existing listener */ -export interface INetworkListener { +export interface INetworkListener extends cdk.IDependable { /** * ARN of the listener */ @@ -134,6 +134,8 @@ export interface NetworkListenerRefProps { * An imported Network Listener */ class ImportedNetworkListener extends cdk.Construct implements INetworkListener { + public readonly dependencyElements: cdk.IDependable[] = []; + /** * ARN of the listener */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts index 5dc2253d84f35..4cb59fbdfad2e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-target-group.ts @@ -2,6 +2,8 @@ import cdk = require('@aws-cdk/cdk'); import { BaseTargetGroup, BaseTargetGroupProps, ITargetGroup, LoadBalancerTargetProps, TargetGroupRefProps } from '../shared/base-target-group'; import { Protocol } from '../shared/enums'; import { BaseImportedTargetGroup } from '../shared/imported'; +import { LazyDependable } from '../shared/util'; +import { INetworkListener } from './network-listener'; /** * Properties for a new Network Target Group @@ -62,6 +64,15 @@ export class NetworkTargetGroup extends BaseTargetGroup { this.addLoadBalancerTarget(result); } } + + /** + * Register a listener that is load balancing to this target group. + * + * Don't call this directly. It will be called by listeners. + */ + public registerListener(listener: INetworkListener) { + this.dependableListeners.push(listener); + } } /** @@ -75,6 +86,9 @@ export interface INetworkTargetGroup extends ITargetGroup { * An imported network target group */ class ImportedNetworkTargetGroup extends BaseImportedTargetGroup implements INetworkTargetGroup { + public listenerDependency(): cdk.IDependable { + return new LazyDependable([]); + } } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index 6bc12566cb6f4..af212853acea7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -5,7 +5,8 @@ import { ITargetGroup } from './base-target-group'; /** * Base class for listeners */ -export abstract class BaseListener extends cdk.Construct { +export abstract class BaseListener extends cdk.Construct implements cdk.IDependable { + public readonly dependencyElements: cdk.IDependable[]; public readonly listenerArn: string; private readonly defaultActions: any[] = []; @@ -17,6 +18,7 @@ export abstract class BaseListener extends cdk.Construct { defaultActions: new cdk.Token(() => this.defaultActions), }); + this.dependencyElements = [resource]; this.listenerArn = resource.ref; } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 356456de0ab26..280b2ab25bd82 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -2,7 +2,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation } from '../elasticloadbalancingv2.generated'; import { Protocol, TargetType } from './enums'; -import { Attributes, renderAttributes } from './util'; +import { Attributes, LazyDependable, renderAttributes } from './util'; /** * Basic properties of both Application and Network Target Groups @@ -145,6 +145,11 @@ export abstract class BaseTargetGroup extends cdk.Construct implements ITargetGr */ protected readonly defaultPort: string; + /** + * List of listeners routing to this target group + */ + protected readonly dependableListeners = new Array(); + /** * Attributes of this target group */ @@ -234,6 +239,13 @@ export abstract class BaseTargetGroup extends cdk.Construct implements ITargetGr this.resource.addDependency(...other); } + /** + * Return an object to depend on the listeners added to this target group + */ + public listenerDependency(): cdk.IDependable { + return new LazyDependable(this.dependableListeners); + } + /** * Register the given load balancing target as part of this group */ @@ -272,6 +284,11 @@ export interface ITargetGroup { * ARN of the target group */ readonly targetGroupArn: string; + + /** + * Return an object to depend on the listeners added to this target group + */ + listenerDependency(): cdk.IDependable; } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts index d992d156a3b2f..808a8f0e2c33e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts @@ -1,3 +1,4 @@ +import cdk = require('@aws-cdk/cdk'); import { ApplicationProtocol } from "./enums"; export type Attributes = {[key: string]: string | undefined}; @@ -67,3 +68,15 @@ export function determineProtocolAndPort(protocol: ApplicationProtocol | undefin export function ifUndefined(x: T | undefined, def: T) { return x !== undefined ? x : def; } + +/** + * Allow lazy evaluation of a list of dependables + */ +export class LazyDependable implements cdk.IDependable { + constructor(private readonly depList: cdk.IDependable[]) { + } + + public get dependencyElements(): cdk.IDependable[] { + return this.depList; + } +} \ No newline at end of file From 1983858b0ebfaf337a4fd118dcff3740a9bbea27 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 11:51:43 +0200 Subject: [PATCH 066/140] Integ test for Fargate service Notes: - Loadbalancer needs to be specified to be "internetFacing" - Cannot modify LoadBalancer on service once service has been created; can get around this by modifying ID of LB - Listener must be set to open --- packages/@aws-cdk/aws-ecs/test/fargate/cdk.json | 9 +++++++++ .../{integ.lb-bridge-nw.ts => integ.lb-awsvpc-nw.ts} | 12 ++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/cdk.json rename packages/@aws-cdk/aws-ecs/test/fargate/{integ.lb-bridge-nw.ts => integ.lb-awsvpc-nw.ts} (72%) diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json new file mode 100644 index 0000000000000..64fd3b2dafe6e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json @@ -0,0 +1,9 @@ +{ + "context": { + "availability-zones:794715269151:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" + ] + } +} diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts similarity index 72% rename from packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts rename to packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index 542975396d13b..fa7e847112bbf 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -4,9 +4,10 @@ import cdk = require('@aws-cdk/cdk'); import ecs = require('../../lib'); const app = new cdk.App(process.argv); -const stack = new cdk.Stack(app, 'aws-ecs-integ'); +const stack = new cdk.Stack(app, 'aws-ecs-integ-fargate'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + const cluster = new ecs.FargateCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { @@ -15,10 +16,9 @@ const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { }); const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - memoryLimitMiB: 1024, }); container.addPortMappings({ - containerPort: 8080, + containerPort: 80, protocol: ecs.Protocol.Tcp }); @@ -27,13 +27,13 @@ const service = new ecs.FargateService(stack, "Service", { taskDefinition, }); -const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); -const listener = lb.addListener('PublicListener', { port: 80 }); +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); +const listener = lb.addListener('PublicListener', { port: 80, open: true }); listener.addTargets('ECS', { port: 80, targets: [service] }); -new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName }); +new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); process.stdout.write(app.run()); \ No newline at end of file From 4ea6b8b70cb9d343e6288ce93686fc8fbe80c073 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 14:03:54 +0200 Subject: [PATCH 067/140] hello-cdk-fargate example works --- .../hello-cdk-fargate/cdk.json | 47 +++++++++++++++++++ .../hello-cdk-fargate/index.ts | 16 +++++-- 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 examples/cdk-examples-typescript/hello-cdk-fargate/cdk.json diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/cdk.json b/examples/cdk-examples-typescript/hello-cdk-fargate/cdk.json new file mode 100644 index 0000000000000..65f81e1c0d3ee --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/cdk.json @@ -0,0 +1,47 @@ +{ + "app": "node index", + "context": { + "availability-zones:585695036304:us-east-1": [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f" + ], + "ssm:585695036304:us-east-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-14c5486b", + "availability-zones:585695036304:eu-west-2": [ + "eu-west-2a", + "eu-west-2b", + "eu-west-2c" + ], + "ssm:585695036304:eu-west-2:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-a36f8dc4", + "availability-zones:585695036304:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" + ], + "ssm:585695036304:eu-west-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-ca0135b3", + "availability-zones:794715269151:us-west-2": [ + "us-west-2a", + "us-west-2b", + "us-west-2c" + ], + "availability-zones:993655754359:us-west-2": [ + "us-west-2a", + "us-west-2b", + "us-west-2c" + ], + "availability-zones:993655754359:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" + ], + "ssm:794715269151:us-west-2:/aws/service/ecs/optimized-ami/amazon-linux/recommended": "{\"schema_version\":1,\"image_name\":\"amzn-ami-2018.03.g-amazon-ecs-optimized\",\"image_id\":\"ami-00430184c7bb49914\",\"os\":\"Amazon Linux\",\"ecs_runtime_version\":\"Docker version 18.06.1-ce\",\"ecs_agent_version\":\"1.20.3\"}", + "availability-zones:794715269151:eu-west-1": [ + "eu-west-1a", + "eu-west-1b", + "eu-west-1c" + ] + } +} diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts index 3561b026fb7b6..83b8e3a0916b0 100644 --- a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts @@ -6,7 +6,10 @@ import cdk = require('@aws-cdk/cdk'); class BonjourFargate extends cdk.Stack { constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { super(parent, name, props); - const vpc = new ec2.VpcNetwork(this, 'VPC'); + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { + maxAZs: 2 // Just to limit the number of resources created and avoid reaching quotas + }); + const cluster = new ecs.FargateCluster(this, 'Cluster', { vpc }); @@ -16,10 +19,14 @@ class BonjourFargate extends cdk.Stack { memoryMiB: '2GB' }); - taskDefinition.addContainer('WebApp', { + const container = taskDefinition.addContainer('WebApp', { // image: new ecs.ImageFromSource('./my-webapp-source'), image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - // portMappings: [{ containerPort: 8080 }], + }); + + container.addPortMappings({ + containerPort: 80, + protocol: ecs.Protocol.Tcp }); const service = new ecs.FargateService(this, 'Service', { @@ -41,6 +48,9 @@ class BonjourFargate extends cdk.Stack { targets: [service], protocol: elbv2.ApplicationProtocol.Http }); + + // This outputs for the DNS where you can access your service + new cdk.Output(this, 'LoadBalancerDNS', { value: loadBalancer.dnsName }); } } From e34d515ea95e6083320e5749d18b991b1d105ad1 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 16:05:03 +0200 Subject: [PATCH 068/140] Add lazy evaluation of network configuration property --- packages/@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 67d8cf58e23c3..9d9b8b7e491e8 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -77,7 +77,7 @@ export abstract class BaseService extends cdk.Construct minimumHealthyPercent: props.minimumHealthyPercent }, /* role: never specified, supplanted by Service Linked Role */ - networkConfiguration: this.networkConfiguration, + networkConfiguration: new cdk.Token(() => this.networkConfiguration), platformVersion: props.platformVersion, ...additionalProps }); From bf90c27c0baa93136bca7b6c9f0a289d287b5970 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 16:07:56 +0200 Subject: [PATCH 069/140] Add LoadBalancedFargateService L3 construct --- .../hello-cdk-fargate/index.ts | 52 ++------ packages/@aws-cdk/aws-ecs/lib/index.ts | 1 + .../lib/load-balanced-fargate-service.ts | 114 ++++++++++++++++++ 3 files changed, 125 insertions(+), 42 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts index 83b8e3a0916b0..03456cf32280f 100644 --- a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts @@ -1,56 +1,24 @@ import ec2 = require('@aws-cdk/aws-ec2'); import ecs = require('@aws-cdk/aws-ecs'); -import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); class BonjourFargate extends cdk.Stack { constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { super(parent, name, props); - const vpc = new ec2.VpcNetwork(this, 'MyVpc', { - maxAZs: 2 // Just to limit the number of resources created and avoid reaching quotas - }); - - const cluster = new ecs.FargateCluster(this, 'Cluster', { - vpc - }); - - const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { - cpu: '512', - memoryMiB: '2GB' - }); - - const container = taskDefinition.addContainer('WebApp', { - // image: new ecs.ImageFromSource('./my-webapp-source'), - image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - }); - container.addPortMappings({ - containerPort: 80, - protocol: ecs.Protocol.Tcp - }); + // Create VPC and Fargate Cluster + // NOTE: Limit AZs to avoid reaching resource quotas + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); + const cluster = new ecs.FargateCluster(this, 'Cluster', { vpc }); - const service = new ecs.FargateService(this, 'Service', { + // Instantiate Fargate Service with just cluster and image + const fargateService = new ecs.LoadBalancedFargateService(this, "FargateService", { cluster, - taskDefinition - }); - - const loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'LB', { - vpc, - internetFacing: true, - }); - - const listener = loadBalancer.addListener('Listener', { - port: 80, - open: true, - }); - - listener.addTargets('DefaultTargets', { - targets: [service], - protocol: elbv2.ApplicationProtocol.Http + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), }); - // This outputs for the DNS where you can access your service - new cdk.Output(this, 'LoadBalancerDNS', { value: loadBalancer.dnsName }); + // Output the DNS where you can access your service + new cdk.Output(this, 'LoadBalancerDNS', { value: fargateService.loadBalancer.dnsName }); } } @@ -58,4 +26,4 @@ const app = new cdk.App(process.argv); new BonjourFargate(app, 'Bonjour'); -process.stdout.write(app.run()); +process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 5c3170c2fe003..892ddfba70418 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -12,6 +12,7 @@ export * from './fargate/fargate-task-definition'; export * from './container-definition'; export * from './container-image'; export * from './linux-parameters'; +export * from './load-balanced-fargate-service'; export * from './log-drivers/log-driver'; export * from './log-drivers/aws-log-driver'; diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts new file mode 100644 index 0000000000000..8d02f178f5c54 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts @@ -0,0 +1,114 @@ +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import { ContainerImage } from './container-image'; +import { FargateCluster } from './fargate/fargate-cluster'; +import { FargateService } from './fargate/fargate-service'; +import { FargateTaskDefinition } from './fargate/fargate-task-definition'; + +export interface LoadBalancedFargateServiceProps { + /** + * The cluster where your Fargate service will be deployed + */ + cluster: FargateCluster; + + image: ContainerImage; + /** + * The number of cpu units used by the task. + * Valid values, which determines your range of valid values for the memory parameter: + * 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB + * 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB + * 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB + * 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments + * 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments + * + * This default is set in the underlying FargateTaskDefinition construct. + * + * @default 256 + */ + cpu?: string; + + /** + * The amount (in MiB) of memory used by the task. + * + * This field is required and you must use one of the following values, which determines your range of valid values + * for the cpu parameter: + * + * 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU) + * + * 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU) + * + * 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU) + * + * Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU) + * + * Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU) + * + * This default is set in the underlying FargateTaskDefinition construct. + * + * @default 512 + */ + memoryMiB?: string; + + /** + * The container port of the application load balancer attached to your Fargate service. Corresponds to container port mapping. + * + * @default 80 + */ + containerPort?: number; + + /** + * Determines whether the Application Load Balancer will be internet-facing + * + * @default true + */ + publicLoadBalancer?: boolean; + + /** + * Determines whether your Fargate Service will be assigned a public IP address. + * + * @default false + */ + publicTasks?: boolean; +} + +export class LoadBalancedFargateService extends cdk.Construct { + public readonly loadBalancer: elbv2.ApplicationLoadBalancer; + + constructor(parent: cdk.Construct, id: string, props: LoadBalancedFargateServiceProps) { + super(parent, id); + + const taskDefinition = new FargateTaskDefinition(this, 'TaskDef', { + memoryMiB: props.memoryMiB, + cpu: props.cpu + }); + + const container = taskDefinition.addContainer('web', { + image: props.image, + }); + + container.addPortMappings({ + containerPort: props.containerPort || 80, + }); + + const assignPublicIp = props.publicTasks !== undefined ? props.publicTasks : false; + const service = new FargateService(this, "Service", { + cluster: props.cluster, + taskDefinition, + assignPublicIp + }); + + const internetFacing = props.publicLoadBalancer !== undefined ? props.publicLoadBalancer : true; + const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { + vpc: props.cluster.vpc, + internetFacing + }); + + this.loadBalancer = lb; + + const listener = lb.addListener('PublicListener', { port: 80, open: true }); + listener.addTargets('ECS', { + port: 80, + targets: [service] + }); + } +} \ No newline at end of file From ffbad9f29ffacb09844206af0489b4f209fd617c Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 4 Oct 2018 16:42:07 +0200 Subject: [PATCH 070/140] Start LoadBalancedFargateServiceApplet --- .../fargate-service.yml | 5 ++ .../load-balanced-fargate-service-applet.ts | 86 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml create mode 100644 packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml new file mode 100644 index 0000000000000..5148b20ae12a1 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml @@ -0,0 +1,5 @@ +# applet is loaded from the local ./test-applet.js file +applet: cdk-ecs:LoadBalancedFargateServiceApplet +image: 'amazon/amazon-ecs-sample' +cpu: 2048 +memoryMiB: 1024 diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts new file mode 100644 index 0000000000000..23fea36a3729c --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts @@ -0,0 +1,86 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { DockerHub } from './container-image'; +import { FargateCluster } from './fargate/fargate-cluster'; +import { LoadBalancedFargateService } from './load-balanced-fargate-service'; + +export interface LoadBalancedFargateServiceAppletProps extends cdk.StackProps { + image: string; + /** + * The number of cpu units used by the task. + * Valid values, which determines your range of valid values for the memory parameter: + * 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB + * 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB + * 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB + * 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments + * 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments + * + * This default is set in the underlying FargateTaskDefinition construct. + * + * @default 256 + */ + cpu?: string; + + /** + * The amount (in MiB) of memory used by the task. + * + * This field is required and you must use one of the following values, which determines your range of valid values + * for the cpu parameter: + * + * 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU) + * + * 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU) + * + * 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU) + * + * Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU) + * + * Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU) + * + * This default is set in the underlying FargateTaskDefinition construct. + * + * @default 512 + */ + memoryMiB?: string; + + /** + * The container port of the application load balancer attached to your Fargate service. Corresponds to container port mapping. + * + * @default 80 + */ + containerPort?: number; + + /** + * Determines whether the Application Load Balancer will be internet-facing + * + * @default true + */ + publicLoadBalancer?: boolean; + + /** + * Determines whether your Fargate Service will be assigned a public IP address. + * + * @default false + */ + publicTasks?: boolean; +} + +export class LoadBalancedFargateServiceApplet extends cdk.Stack { + constructor(parent: cdk.App, id: string, props: LoadBalancedFargateServiceAppletProps) { + super(parent, id, props); + + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); + const cluster = new FargateCluster(this, 'Cluster', { vpc }); + + // Instantiate Fargate Service with just cluster and image + new LoadBalancedFargateService(this, "FargateService", { + cluster, + cpu: props.cpu, + containerPort: props.containerPort, + memoryMiB: props.memoryMiB, + publicLoadBalancer: props.publicLoadBalancer, + publicTasks: props.publicTasks, + image: DockerHub.image(props.image), + }); + } +} \ No newline at end of file From d1b22bd29cdbf8b9e6352d5a441a9fe1e5d1cff6 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 4 Oct 2018 16:42:18 +0200 Subject: [PATCH 071/140] Application AutoScaling WIP --- .../lib/base-scaling-policy.ts | 54 +++++ .../aws-applicationautoscaling/lib/index.ts | 4 + .../lib/scalable-target.ts | 189 ++++++++++++++++++ .../lib/step-scaling-policy.ts | 130 ++++++++++++ .../aws-applicationautoscaling/package.json | 3 +- 5 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-applicationautoscaling/lib/base-scaling-policy.ts create mode 100644 packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts create mode 100644 packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scaling-policy.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scaling-policy.ts new file mode 100644 index 0000000000000..3aca090aab883 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scaling-policy.ts @@ -0,0 +1,54 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from './applicationautoscaling.generated'; +import { IScalableTarget } from './scalable-target'; + +/** + * Properties for a scaling policy + */ +export interface BaseScalingPolicyProps { + /** + * A name for the scaling policy + * + * @default Automatically generated name + */ + policyName?: string; + + /** + * The scalable target + */ + scalingTarget: IScalableTarget; +} + +export abstract class BaseScalingPolicy extends cdk.Construct { + public readonly scalingPolicyArn: string; + + constructor(parent: cdk.Construct, id: string, props: BaseScalingPolicyProps, additionalProps: any) { + super(parent, id); + + // scalingTargetId == "" means look at the other properties + const scalingTargetId = props.scalingTarget.scalableTargetId !== "" ? props.scalingTarget.scalableTargetId : undefined; + let resourceId; + let scalableDimension; + let serviceNamespace; + if (scalingTargetId === undefined) { + if (props.scalingTarget.scalableDimension === "" || props.scalingTarget.resourceId === "" + || props.scalingTarget.serviceNamespace === "") { + throw new Error(`A scaling target requires either a 'scalableTargetId' or all of 'resourceId', 'scalableDimension', 'serviceNamespace'`); + } + resourceId = props.scalingTarget.resourceId; + scalableDimension = props.scalingTarget.scalableDimension; + serviceNamespace = props.scalingTarget.serviceNamespace; + } + + const resource = new cloudformation.ScalingPolicyResource(this, 'Resource', { + policyName: props.policyName || this.uniqueId, + scalingTargetId, + resourceId, + scalableDimension, + serviceNamespace, + ...additionalProps + }); + + this.scalingPolicyArn = resource.scalingPolicyArn; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/index.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/index.ts index 5f831ad820f97..40dc603cd0132 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/index.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/index.ts @@ -1,2 +1,6 @@ // AWS::ApplicationAutoScaling CloudFormation Resources: export * from './applicationautoscaling.generated'; + +export * from './scalable-target'; +export * from './base-scaling-policy'; +export * from './step-scaling-policy'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts new file mode 100644 index 0000000000000..3609d0343d148 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts @@ -0,0 +1,189 @@ +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from './applicationautoscaling.generated'; + +/** + * Properties for a scalable target + */ +export interface ScalableTargetProps { + /** + * The minimum value that Application Auto Scaling can use to scale a target during a scaling activity. + */ + minCapacity: number; + + /** + * The maximum value that Application Auto Scaling can use to scale a target during a scaling activity. + */ + maxCapacity: number; + + /** + * Role that allows Application Auto Scaling to modify your scalable target. + * + * If not supplied, a service-linked role is used. Some resources require a + * concrete role. + * + * @default A service-linked role is used + */ + role?: iam.Role; + + /** + * The resource identifier to associate with this scalable target. + * + * This string consists of the resource type and unique identifier. + * + * @example service/ecsStack-MyECSCluster-AB12CDE3F4GH/ecsStack-MyECSService-AB12CDE3F4GH + * @see https://docs.aws.amazon.com/autoscaling/application/APIReference/API_RegisterScalableTarget.html + */ + resourceId: string; + + /** + * The scalable dimension that's associated with the scalable target. + * + * Specify the service namespace, resource type, and scaling property. + * + * @example ecs:service:DesiredCount + * @see https://docs.aws.amazon.com/autoscaling/application/APIReference/API_ScalingPolicy.html + */ + scalableDimension: string; + + /** + * The namespace of the AWS service that provides the resource or + * custom-resource for a resource provided by your own application or + * service. + * + * For valid AWS service namespace values, see the RegisterScalableTarget + * action in the Application Auto Scaling API Reference. + * + * @see https://docs.aws.amazon.com/autoscaling/application/APIReference/API_RegisterScalableTarget.html + */ + serviceNamespace: string; +} + +/** + * Define a scalable target + */ +export class ScalableTarget extends cdk.Construct implements IScalableTarget { + /** + * ID of the Scalable Target + * + * @example service/ecsStack-MyECSCluster-AB12CDE3F4GH/ecsStack-MyECSService-AB12CDE3F4GH|ecs:service:DesiredCount|ecs + */ + public readonly scalableTargetId: string; + + public readonly resourceId: string; + public readonly scalableDimension: string; + public readonly serviceNamespace: string; + + private readonly actions = new Array(); + + constructor(parent: cdk.Construct, id: string, props: ScalableTargetProps) { + super(parent, id); + + const resource = new cloudformation.ScalableTargetResource(this, 'Resource', { + maxCapacity: props.maxCapacity, + minCapacity: props.minCapacity, + resourceId: props.resourceId, + // Schema says roleArn is required but docs say it's not. Override typechecking. + roleArn: (props.role && props.role.roleArn) as any, + scalableDimension: props.scalableDimension, + scheduledActions: this.actions, + serviceNamespace: props.serviceNamespace + }); + + this.resourceId = props.resourceId; + this.scalableDimension = props.scalableDimension; + this.serviceNamespace = props.serviceNamespace; + this.scalableTargetId = resource.scalableTargetId; + } + + /** + * Schedule an action for this scalable target + */ + public addScheduledAction(action: ScheduledAction) { + if (action.minCapacity === undefined && action.maxCapacity === undefined) { + throw new Error('You must supply at least one of minCapacity or maxCapacity'); + } + this.actions.push({ + scheduledActionName: action.name, + schedule: action.schedule, + startTime: action.startTime, + endTime: action.endTime, + scalableTargetAction: { + maxCapacity: action.maxCapacity, + minCapacity: action.minCapacity + }, + }); + } +} + +export interface ScheduledAction { + /** + * A name for the scheduled action + */ + name: string; + + /** + * When to perform this action. + * + * Support formats: + * - at(yyyy-mm-ddThh:mm:ss) + * - rate(value unit) + * - cron(fields) + * + * "At" expressions are useful for one-time schedules. Specify the time in + * UTC. + * + * For "rate" expressions, value is a positive integer, and unit is minute, + * minutes, hour, hours, day, or days. + * + * For more information about cron expressions, see https://en.wikipedia.org/wiki/Cron. + * + * @example rate(12 hours) + */ + schedule: string; + + /** + * When this scheduled action becomes active. + * + * @default The rule is activate immediately + */ + startTime?: Date + + /** + * When this scheduled action expires. + * + * @default The rule never expires. + */ + endTime?: Date; + + /** + * The new minimum capacity. + * + * During the scheduled time, if the current capacity is below the minimum + * capacity, Application Auto Scaling scales out to the minimum capacity. + * + * At least one of maxCapacity and minCapacity must be supplied. + * + * @default No new minimum capacity + */ + minCapacity?: number; + + /** + * The new maximum capacity. + * + * During the scheduled time, the current capacity is above the maximum + * capacity, Application Auto Scaling scales in to the maximum capacity. + * + * At least one of maxCapacity and minCapacity must be supplied. + * + * @default No new maximum capacity + */ + maxCapacity?: number; +} + +export interface IScalableTarget { + readonly scalableTargetId: string; + readonly resourceId: string; + readonly scalableDimension: string; + readonly serviceNamespace: string; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts new file mode 100644 index 0000000000000..1b3ea03fd4e55 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts @@ -0,0 +1,130 @@ +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from './applicationautoscaling.generated'; +import { BaseScalingPolicy, BaseScalingPolicyProps } from "./base-scaling-policy"; + +/** + * Properties for a scaling policy + */ +export interface StepScalingPolicyProps extends BaseScalingPolicyProps { + /** + * How the adjustment numbers are interpreted + * + * @default ChangeInCapacity + */ + adjustmentType?: AdjustmentType; + + /** + * Grace period after scaling activity. + * + * For scale out policies, multiple scale outs during the cooldown period are + * squashed so that only the biggest scale out happens. + * + * For scale in policies, subsequent scale ins during the cooldown period are + * ignored. + * + * @see https://docs.aws.amazon.com/autoscaling/application/APIReference/API_StepScalingPolicyConfiguration.html + * @default No cooldown period + */ + cooldownSeconds?: number; + + /** + * Minimum absolute number to adjust capacity with as result of percentage scaling. + * + * Only when using AdjustmentType = PercentChangeInCapacity, this number controls + * the minimum absolute effect size. + * + * @default No minimum scaling effect + */ + minAdjustmentMagnitude?: number; + + /** + * The aggregation type for the CloudWatch metrics. + * + * @default Average + */ + metricAggregationType?: MetricAggregationType; +} + +/** + * Define a step scaling policy + * + * This kind of scaling policy adjusts the target capacity in configurable + * steps. The size of the step is configurable based on the metric's distance + * to its alarm threshold. + */ +export class StepScalingPolicy extends BaseScalingPolicy implements cloudwatch.IAlarmAction { + public readonly alarmActionArn: string; + private readonly adjustments = new Array(); + + constructor(parent: cdk.Construct, id: string, props: StepScalingPolicyProps) { + super(parent, id, props, { + policyType: 'StepScaling', + stepScalingPolicyConfiguration: { + adjustmentType: props.adjustmentType, + cooldown: props.cooldownSeconds, + minAdjustmentMagnitude: props.minAdjustmentMagnitude, + metricAggregationType: props.metricAggregationType, + stepAdjustments: new cdk.Token(() => this.adjustments), + } as cloudformation.ScalingPolicyResource.StepScalingPolicyConfigurationProperty + }); + this.alarmActionArn = this.scalingPolicyArn; + } +} + +/** + * How adjustment numbers are interpreted + */ +export enum AdjustmentType { + /** + * Add the adjustment number to the current capacity. + * + * A positive number increases capacity, a negative number decreases capacity. + */ + ChangeInCapacity = 'ChangeInCapacity', + + /** + * Add this percentage of the current capacity to itself. + * + * The number must be between -100 and 100; a positive number increases + * capacity and a negative number decreases it. + */ + PercentChangeInCapacity = 'PercentChangeInCapacity', + + /** + * Make the capacity equal to the exact number given. + */ + ExactCapacity = 'ExactCapacity', +} + +/** + * How the scaling metric is going to be aggregated + */ +export enum MetricAggregationType { + /** + * Average + */ + Average = 'Average', + + /** + * Minimum + */ + Minimum = 'Minimum', + + /** + * Maximum + */ + Maximum = 'Maximum' +} + +export interface ITierRange { + scaleBy(adjustment: number): ITierMark; +} + +export interface ITierMark { + at(threshold: number): ITierRange; + + endAt0(): StepScalingPolicy; + + endAtInfinity(): StepScalingPolicy; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index cd5a071eaf1e5..5de930f16c18b 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -58,7 +58,8 @@ "pkglint": "^0.10.0" }, "dependencies": { - "@aws-cdk/cdk": "^0.10.0" + "@aws-cdk/cdk": "^0.10.0", + "@aws-cdk/aws-cloudwatch": "^0.10.0" }, "homepage": "https://github.com/awslabs/aws-cdk" } From d1ec640c5ce8368f274f485c7d2e204dbe376d62 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 4 Oct 2018 16:51:18 +0200 Subject: [PATCH 072/140] Make declarative example work --- .../hello-cdk-ecs-declarative/cdk.json | 3 +++ .../hello-cdk-ecs-declarative/fargate-service.yml | 6 +++--- examples/cdk-examples-typescript/package.json | 1 + packages/@aws-cdk/aws-ecs/lib/index.ts | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 examples/cdk-examples-typescript/hello-cdk-ecs-declarative/cdk.json diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/cdk.json b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/cdk.json new file mode 100644 index 0000000000000..01cdce7b82735 --- /dev/null +++ b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "../node_modules/.bin/cdk-applet-js fargate-service.yml" +} diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml index 5148b20ae12a1..b00e3c4b0eb4c 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml +++ b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml @@ -1,5 +1,5 @@ # applet is loaded from the local ./test-applet.js file -applet: cdk-ecs:LoadBalancedFargateServiceApplet +applet: @aws-cdk/aws-ecs:LoadBalancedFargateServiceApplet image: 'amazon/amazon-ecs-sample' -cpu: 2048 -memoryMiB: 1024 +cpu: "2048" +memoryMiB: "1024" diff --git a/examples/cdk-examples-typescript/package.json b/examples/cdk-examples-typescript/package.json index 79b2fd677a62a..08c5b33d82334 100644 --- a/examples/cdk-examples-typescript/package.json +++ b/examples/cdk-examples-typescript/package.json @@ -23,6 +23,7 @@ "pkglint": "^0.10.0" }, "dependencies": { + "@aws-cdk/applet-js": "^0.10.0", "@aws-cdk/aws-autoscaling": "^0.10.0", "@aws-cdk/aws-cloudformation": "^0.10.0", "@aws-cdk/aws-cognito": "^0.10.0", diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 892ddfba70418..0c06c5155f709 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -13,6 +13,7 @@ export * from './container-definition'; export * from './container-image'; export * from './linux-parameters'; export * from './load-balanced-fargate-service'; +export * from './load-balanced-fargate-service-applet'; export * from './log-drivers/log-driver'; export * from './log-drivers/aws-log-driver'; From 36e1ba281ad367e6fb3925d2cab0a36991d9a178 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 12:17:51 +0200 Subject: [PATCH 073/140] Add import exports to Cluster --- .../@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 54 ++++++++++++++++--- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 6 +-- .../aws-ecs/lib/fargate/fargate-cluster.ts | 39 +++++++++++++- .../aws-ecs/lib/fargate/fargate-service.ts | 4 +- .../lib/load-balanced-fargate-service.ts | 4 +- 5 files changed, 91 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index 20928345a8f00..b180cc8003838 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -12,8 +12,13 @@ export interface EcsClusterProps extends BaseClusterProps { containersAccessInstanceRole?: boolean; } -export class EcsCluster extends BaseCluster { +export class EcsCluster extends BaseCluster implements IEcsCluster { + public static import(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps): IEcsCluster { + return new ImportedEcsCluster(parent, name, props); + } + public readonly autoScalingGroup: autoscaling.AutoScalingGroup; + public readonly securityGroup: ec2.SecurityGroupRef; constructor(parent: cdk.Construct, name: string, props: EcsClusterProps) { super(parent, name, props); @@ -25,6 +30,8 @@ export class EcsCluster extends BaseCluster { updateType: autoscaling.UpdateType.ReplacingUpdate }); + this.securityGroup = autoScalingGroup.connections.securityGroup!; + // Tie instances to cluster autoScalingGroup.addUserData(`echo ECS_CLUSTER=${this.clusterName} >> /etc/ecs/ecs.config`); @@ -73,13 +80,16 @@ export class EcsCluster extends BaseCluster { this.autoScalingGroup = autoScalingGroup; } - // public runService(taskDefinition: EcsTaskDefinition): EcsService { - // return new Service(this, `${taskDefinition.family}Service`, { - // cluster: this, - // taskDefinition, - // // FIXME: additional props? Or set on Service object? - // }); - // } + /** + * Export the EcsCluster + */ + public export(): ImportedEcsClusterProps { + return { + clusterName: new cdk.Output(this, 'ClusterName', { value: this.clusterName }).makeImportValue().toString(), + vpc: this.vpc.export(), + securityGroup: this.securityGroup.export(), + }; + } } /** @@ -97,3 +107,31 @@ export class EcsOptimizedAmi implements ec2.IMachineImageSource { return new ec2.MachineImage(ami, new ec2.LinuxOS()); } } + +export interface IEcsCluster { + readonly clusterName: string; + readonly vpc: ec2.VpcNetworkRef; + readonly securityGroup: ec2.SecurityGroupRef; +} + +export interface ImportedEcsClusterProps { + readonly clusterName: string; + readonly vpc: ec2.VpcNetworkRefProps; + readonly securityGroup: ec2.SecurityGroupRefProps; +} + +// /** +// * A EcsCluster that has been imported +// */ +class ImportedEcsCluster extends cdk.Construct implements IEcsCluster { + public readonly clusterName: string; + public readonly vpc: ec2.VpcNetworkRef; + public readonly securityGroup: ec2.SecurityGroupRef; + + constructor(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps) { + super(parent, name); + this.clusterName = props.clusterName; + this.vpc = ec2.VpcNetworkRef.import(this, "vpc", props.vpc); + this.securityGroup = ec2.SecurityGroupRef.import(this, "securityGroup", props.securityGroup); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index f389d4b0b45a4..49a30ca38f3fe 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -4,14 +4,14 @@ import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; -import { EcsCluster } from './ecs-cluster'; +import { IEcsCluster } from './ecs-cluster'; import { EcsTaskDefinition } from './ecs-task-definition'; export interface EcsServiceProps extends BaseServiceProps { /** * Cluster where service will be deployed */ - cluster: EcsCluster; // should be required? do we assume 'default' exists? + cluster: IEcsCluster; // should be required? do we assume 'default' exists? /** * Task Definition used for running tasks in the service @@ -85,7 +85,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { } else { // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. validateNoNetworkingProps(props); - this._securityGroup = props.cluster.autoScalingGroup.connections.securityGroup!; + this._securityGroup = props.cluster.securityGroup!; } this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts index b928fb8be616f..a92f990657592 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts @@ -1,3 +1,4 @@ +import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; @@ -5,8 +6,44 @@ import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; export interface FargateClusterProps extends BaseClusterProps { } -export class FargateCluster extends BaseCluster { +export class FargateCluster extends BaseCluster implements IFargateCluster { + public static import(parent: cdk.Construct, name: string, props: ImportedFargateClusterProps): IFargateCluster { + return new ImportedFargateCluster(parent, name, props); + } constructor(parent: cdk.Construct, name: string, props: FargateClusterProps) { super(parent, name, props); } + /** + * Export the FargateCluster + */ + public export(): ImportedFargateClusterProps { + return { + clusterName: new cdk.Output(this, 'ClusterName', { value: this.clusterName }).makeImportValue().toString(), + vpc: this.vpc.export(), + }; + } } + +export interface IFargateCluster { + clusterName: string; + vpc: ec2.VpcNetworkRef; +} + +export interface ImportedFargateClusterProps { + readonly clusterName: string; + readonly vpc: ec2.VpcNetworkRefProps; +} + +// /** +// * A FargateCluster that has been imported +// */ +class ImportedFargateCluster extends cdk.Construct implements IFargateCluster { + public readonly clusterName: string; + public readonly vpc: ec2.VpcNetworkRef; + + constructor(parent: cdk.Construct, name: string, props: ImportedFargateClusterProps) { + super(parent, name); + this.clusterName = props.clusterName; + this.vpc = ec2.VpcNetworkRef.import(this, "vpc", props.vpc); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index d1a490c859c2b..8144ae913607e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -2,14 +2,14 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; import { BaseTaskDefinition } from '../base/base-task-definition'; -import { FargateCluster } from './fargate-cluster'; +import { IFargateCluster } from './fargate-cluster'; import { FargateTaskDefinition } from './fargate-task-definition'; export interface FargateServiceProps extends BaseServiceProps { /** * Cluster where service will be deployed */ - cluster: FargateCluster; // should be required? do we assume 'default' exists? + cluster: IFargateCluster; // should be required? do we assume 'default' exists? /** * Task Definition used for running tasks in the service diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts index 8d02f178f5c54..04cae422cc05d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts @@ -1,7 +1,7 @@ import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); import { ContainerImage } from './container-image'; -import { FargateCluster } from './fargate/fargate-cluster'; +import { IFargateCluster } from './fargate/fargate-cluster'; import { FargateService } from './fargate/fargate-service'; import { FargateTaskDefinition } from './fargate/fargate-task-definition'; @@ -9,7 +9,7 @@ export interface LoadBalancedFargateServiceProps { /** * The cluster where your Fargate service will be deployed */ - cluster: FargateCluster; + cluster: IFargateCluster; image: ContainerImage; /** From 178898f6f8ff8e2c6c6f5e12236420dcdcad7e8c Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 15:11:56 +0200 Subject: [PATCH 074/140] Fix portMappings/ingressPort --- .../hello-cdk-ecs/index.ts | 2 +- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 15 +++++++-------- .../@aws-cdk/aws-ecs/lib/container-definition.ts | 14 ++++++++------ packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 7 ++++++- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 97ba362de1b9c..a433860efef58 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -68,7 +68,7 @@ class BonjourECS extends cdk.Stack { container.addPortMappings({ containerPort: 80, - hostPort: 80, + // hostPort: 80, protocol: ecs.Protocol.Tcp, }); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 9d9b8b7e491e8..a26a49030fe43 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -90,7 +90,7 @@ export abstract class BaseService extends cdk.Construct // Open up security groups. For dynamic port mapping, we won't know the port range // in advance so we need to open up all ports. - const port = this.instancePort; + const port = this.containerPort; const portRange = port === 0 ? EPHEMERAL_PORT_RANGE : new ec2.TcpPort(port); targetGroup.registerConnectable(this, portRange); @@ -133,21 +133,20 @@ export abstract class BaseService extends cdk.Construct this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, containerName: this.taskDef.defaultContainer!.id, - containerPort: this.instancePort, + containerPort: this.containerPort, }); this.resource.addDependency(targetGroup.listenerDependency()); - return { targetType: elbv2.TargetType.Ip }; + const targetType = this.taskDef.networkMode === NetworkMode.AwsVpc ? elbv2.TargetType.Ip : elbv2.TargetType.Instance; + return { targetType }; } /** - * Return the port on which the instance will be listening - * - * Returns 0 if the networking mode implies dynamic port allocation. + * Return the port on which the load balancer will be listening */ - private get instancePort() { - return this.taskDef.networkMode === NetworkMode.Bridge ? 0 : this.taskDef.defaultContainer!.ingressPort; + private get containerPort() { + return this.taskDef.defaultContainer!.ingressPort; } } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index ed1af1f3727dc..1146ade212515 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -214,6 +214,11 @@ export class ContainerDefinition extends cdk.Construct { throw new Error(`Host port ${pm.hostPort} does not match container port ${pm.containerPort}.`); } } + if (this.taskDefinition.networkMode === NetworkMode.Bridge) { + if (pm.hostPort === undefined) { + pm.hostPort = 0; + } + } } this.portMappings.push(...portMappings); } @@ -245,12 +250,9 @@ export class ContainerDefinition extends cdk.Construct { throw new Error(`Container ${this.id} hasn't defined any ports`); } const defaultPortMapping = this.portMappings[0]; - if (defaultPortMapping.hostPort !== undefined) { - return defaultPortMapping.hostPort; - } - if (this.taskDefinition.networkMode === NetworkMode.Bridge) { - return 0; - } + // if (defaultPortMapping.hostPort !== undefined && defaultPortMapping.hostPort !== 0) { + // return defaultPortMapping.hostPort; + // } return defaultPortMapping.containerPort; } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index b180cc8003838..f82ea5b5f513b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -10,6 +10,11 @@ export interface EcsClusterProps extends BaseClusterProps { * @default false */ containersAccessInstanceRole?: boolean; + + /** + * The type of EC2 instance to launch into your Autoscaling Group + */ + instanceType?: ec2.InstanceType; } export class EcsCluster extends BaseCluster implements IEcsCluster { @@ -25,7 +30,7 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'AutoScalingGroup', { vpc: props.vpc, - instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), + instanceType: props.instanceType || new ec2.InstanceTypePair(ec2.InstanceClass.T2, ec2.InstanceSize.Micro), machineImage: new EcsOptimizedAmi(), updateType: autoscaling.UpdateType.ReplacingUpdate }); From b73bdad4fed4e831114826291c2cc88e05986403 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 15:13:11 +0200 Subject: [PATCH 075/140] Clean up fargate integ test --- .../aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index 54d49109ae3b5..4b7cb5f6ca163 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -4,23 +4,21 @@ import cdk = require('@aws-cdk/cdk'); import ecs = require('../../lib'); const app = new cdk.App(process.argv); -const stack = new cdk.Stack(app, 'aws-ecs-integ-fargate'); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -const cluster = new ecs.FargateCluster(stack, 'EcsCluster', { vpc }); +const cluster = new ecs.FargateCluster(stack, 'FargateCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { memoryMiB: '1GB', cpu: '512' }); + const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), }); -container.addPortMappings({ - containerPort: 80, - protocol: ecs.Protocol.Tcp -}); + container.addPortMappings({ containerPort: 80, protocol: ecs.Protocol.Tcp @@ -33,7 +31,7 @@ const service = new ecs.FargateService(stack, "Service", { const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); const listener = lb.addListener('PublicListener', { port: 80, open: true }); -listener.addTargets('ECS', { +listener.addTargets('Fargate', { port: 80, targets: [service] }); From 049557ebbea8ecdd7a28116b819b5d16b70b61de Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 15:14:27 +0200 Subject: [PATCH 076/140] Add integ test for load balanced service in bridge mode --- .../aws-ecs/test/ecs/integ.lb-bridge-nw.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts new file mode 100644 index 0000000000000..8769e055699ea --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts @@ -0,0 +1,43 @@ + +import ec2 = require('@aws-cdk/aws-ec2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import ecs = require('../../lib'); + +const app = new cdk.App(process.argv); +const stack = new cdk.Stack(app, 'aws-ecs-integ-ecs'); + +const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + +const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + +const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + // networkMode defaults to "bridge" + // memoryMiB: '1GB', + // cpu: '512' +}); + +const container = taskDefinition.addContainer('web', { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 1024, +}); +container.addPortMappings({ + containerPort: 80, + protocol: ecs.Protocol.Tcp +}); + +const service = new ecs.EcsService(stack, "Service", { + cluster, + taskDefinition, +}); + +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); +const listener = lb.addListener('PublicListener', { port: 80, open: true }); +listener.addTargets('ECS', { + port: 80, + targets: [service] +}); + +new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); + +process.stdout.write(app.run()); \ No newline at end of file From 2295ef253da4cf43e7c2c5d0e4851aeada935ffd Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 15:54:03 +0200 Subject: [PATCH 077/140] Add integ test for awsvpc network mode on EcsCluster --- .../aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts new file mode 100644 index 0000000000000..a81d9c8932999 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts @@ -0,0 +1,42 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import ecs = require('../../lib'); +import { NetworkMode } from '../../lib'; + +const app = new cdk.App(process.argv); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); + +const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + +const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + +const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + networkMode: NetworkMode.AwsVpc +}); + +const container = taskDefinition.addContainer('web', { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 256, +}); + +container.addPortMappings({ + containerPort: 80, + protocol: ecs.Protocol.Tcp +}); + +const service = new ecs.EcsService(stack, "Service", { + cluster, + taskDefinition, +}); + +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); +const listener = lb.addListener('PublicListener', { port: 80, open: true }); +listener.addTargets('ECS', { + port: 80, + targets: [service] +}); + +new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); + +process.stdout.write(app.run()); \ No newline at end of file From f34c23caffbb190fd160039d9574ef09179d446d Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 16:54:00 +0200 Subject: [PATCH 078/140] Fix ingress/containerPort on LBs Allows bridge network mode to work --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 11 ++-------- .../aws-ecs/lib/container-definition.ts | 20 +++++++++++++++---- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 2 +- .../aws-ecs/test/ecs/integ.lb-bridge-nw.ts | 2 +- .../aws-ecs/test/test.container-definition.ts | 8 ++++---- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index a26a49030fe43..170890b3df45e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -90,7 +90,7 @@ export abstract class BaseService extends cdk.Construct // Open up security groups. For dynamic port mapping, we won't know the port range // in advance so we need to open up all ports. - const port = this.containerPort; + const port = this.taskDef.defaultContainer!.ingressPort; const portRange = port === 0 ? EPHEMERAL_PORT_RANGE : new ec2.TcpPort(port); targetGroup.registerConnectable(this, portRange); @@ -133,7 +133,7 @@ export abstract class BaseService extends cdk.Construct this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, containerName: this.taskDef.defaultContainer!.id, - containerPort: this.containerPort, + containerPort: this.taskDef.defaultContainer!.containerPort, }); this.resource.addDependency(targetGroup.listenerDependency()); @@ -141,13 +141,6 @@ export abstract class BaseService extends cdk.Construct const targetType = this.taskDef.networkMode === NetworkMode.AwsVpc ? elbv2.TargetType.Ip : elbv2.TargetType.Instance; return { targetType }; } - - /** - * Return the port on which the load balancer will be listening - */ - private get containerPort() { - return this.taskDef.defaultContainer!.ingressPort; - } } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 1146ade212515..9d8f563fd4d5c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -242,17 +242,29 @@ export class ContainerDefinition extends cdk.Construct { return this._usesEcrImages; } + public get ingressPort(): number { + if (this.portMappings.length === 0) { + throw new Error(`Container ${this.id} hasn't defined any ports`); + } + const defaultPortMapping = this.portMappings[0]; + + if (defaultPortMapping.hostPort !== undefined && defaultPortMapping.hostPort !== 0) { + return defaultPortMapping.hostPort; + } + + if (this.taskDefinition.networkMode === NetworkMode.Bridge) { + return 0; + } + return defaultPortMapping.containerPort; + } /** * Return the instance port that the container will be listening on */ - public get ingressPort(): number { + public get containerPort(): number { if (this.portMappings.length === 0) { throw new Error(`Container ${this.id} hasn't defined any ports`); } const defaultPortMapping = this.portMappings[0]; - // if (defaultPortMapping.hostPort !== undefined && defaultPortMapping.hostPort !== 0) { - // return defaultPortMapping.hostPort; - // } return defaultPortMapping.containerPort; } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 49a30ca38f3fe..33630da2fb207 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -177,7 +177,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { this.loadBalancers.push({ loadBalancerName: loadBalancer.loadBalancerName, containerName: this.taskDefinition.defaultContainer!.id, - containerPort: this.taskDefinition.defaultContainer!.ingressPort, + containerPort: this.taskDefinition.defaultContainer!.containerPort, }); } } diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts index 8769e055699ea..52e548daf620b 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts @@ -19,7 +19,7 @@ const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - memoryLimitMiB: 1024, + memoryLimitMiB: 256, }); container.addPortMappings({ containerPort: 80, diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index 06476589066a4..d91ec616f9c3c 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -123,7 +123,7 @@ export = { container.addPortMappings({ containerPort: 8080, }); - const actual = container.ingressPort; + const actual = container.containerPort; // THEN const expected = 8080; @@ -148,7 +148,7 @@ export = { container.addPortMappings({ containerPort: 8080, }); - const actual = container.ingressPort; + const actual = container.containerPort; // THEN const expected = 8080; @@ -174,7 +174,7 @@ export = { containerPort: 8081, hostPort: 8080, }); - const actual = container.ingressPort; + const actual = container.containerPort; // THEN const expected = 8080; @@ -197,7 +197,7 @@ export = { container.addPortMappings({ containerPort: 8081, }); - const actual = container.ingressPort; + const actual = container.containerPort; // THEN const expected = 0; From c491a1d9abb75ed77c576ab9e18ae68f657b5886 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 16:55:56 +0200 Subject: [PATCH 079/140] Cleanup example --- examples/cdk-examples-typescript/hello-cdk-ecs/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index a433860efef58..0c1bc12c58fd3 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -92,7 +92,6 @@ class BonjourECS extends cdk.Stack { taskDefinition, desiredCount: 1, }); - // cluster.runService(taskDefinition); } } From 430f81ed4ae39ba25bf0196f38cc47e45024d41c Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 18:11:18 +0200 Subject: [PATCH 080/140] Add cloudwatch metrics --- .../@aws-cdk/aws-ecs/lib/base/base-cluster.ts | 15 ++++++++- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 17 +++++++++- .../@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 31 +++++++++++++++++ .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 33 +++++++++++++++++++ packages/@aws-cdk/aws-ecs/package.json | 1 + 5 files changed, 95 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts index b9944cf4e1344..fe2d923994951 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts @@ -1,3 +1,4 @@ +import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation } from '../ecs.generated'; @@ -35,4 +36,16 @@ export class BaseCluster extends cdk.Construct { this.clusterArn = cluster.clusterArn; this.clusterName = cluster.ref; } -} + + /** + * Return the given named metric for this Cluster + */ + public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ECS', + metricName, + dimensions: { ClusterName: this.clusterName }, + ...props + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 170890b3df45e..978fd6bf74619 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -1,3 +1,4 @@ +import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); @@ -59,6 +60,7 @@ export abstract class BaseService extends cdk.Construct implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { public readonly dependencyElements: cdk.IDependable[]; public abstract readonly connections: ec2.Connections; + public readonly serviceName: string; protected loadBalancers = new Array(); protected networkConfiguration?: cloudformation.ServiceResource.NetworkConfigurationProperty; protected readonly abstract taskDef: BaseTaskDefinition; @@ -83,6 +85,7 @@ export abstract class BaseService extends cdk.Construct }); this.dependencyElements = [this.resource]; + this.serviceName = this.resource.serviceName; } public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { @@ -105,6 +108,18 @@ export abstract class BaseService extends cdk.Construct return this._securityGroup!; } + /** + * Return the given named metric for this Service + */ + public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ECS', + metricName, + dimensions: { ServiceName: this.serviceName }, + ...props + }); + } + // tslint:disable-next-line:max-line-length protected configureAwsVpcNetworking(vpc: ec2.VpcNetworkRef, assignPublicIp?: boolean, vpcPlacement?: ec2.VpcPlacementStrategy, securityGroup?: ec2.SecurityGroupRef) { if (vpcPlacement === undefined) { @@ -179,4 +194,4 @@ export enum FargatePlatformVersion { * Based on Amazon Linux 2017.09. */ Version10 = '1.0.0', -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index f82ea5b5f513b..324d02c123221 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -1,4 +1,5 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); +import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; @@ -95,6 +96,36 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { securityGroup: this.securityGroup.export(), }; } + + /** + * Metric for cluster CPU reservation + * + * @default average over 5 minutes + */ + public metricCpuReservation(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('CPUReservation', props); + } + + /** + * Metric for cluster Memory reservation + * + * @default average over 5 minutes + */ + public metricMemoryReservation(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('MemoryReservation', props ); + } + + /** + * Return the given named metric for this Cluster + */ + public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ECS', + metricName, + dimensions: { ClusterName: this.clusterName }, + ...props + }); + } } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 33630da2fb207..2821d23c8da27 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -1,3 +1,4 @@ +import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import elb = require('@aws-cdk/aws-elasticloadbalancing'); import cdk = require('@aws-cdk/cdk'); @@ -56,6 +57,7 @@ export interface EcsServiceProps extends BaseServiceProps { export class EcsService extends BaseService implements elb.ILoadBalancerTarget { public readonly connections: ec2.Connections; + public readonly clusterName: string; protected readonly taskDef: BaseTaskDefinition; private readonly taskDefinition: EcsTaskDefinition; private readonly constraints: cloudformation.ServiceResource.PlacementConstraintProperty[]; @@ -76,6 +78,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { schedulingStrategy: props.daemon ? 'DAEMON' : 'REPLICA', }); + this.clusterName = props.cluster.clusterName; this.constraints = []; this.strategies = []; this.daemon = props.daemon || false; @@ -180,6 +183,36 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { containerPort: this.taskDefinition.defaultContainer!.containerPort, }); } + + /** + * Return the given named metric for this Service + */ + public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ECS', + metricName, + dimensions: { ClusterName: this.clusterName, ServiceName: this.serviceName }, + ...props + }); + } + + /** + * Metric for cluster Memory utilization + * + * @default average over 5 minutes + */ + public metricMemoryUtilization(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('MemoryUtilization', props ); + } + + /** + * Metric for cluster CPU utilization + * + * @default average over 5 minutes + */ + public metricCpuUtilization(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('CPUUtilization', props); + } } function validateNoNetworkingProps(props: EcsServiceProps) { diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index e6c82d1e6e64a..a95f312f7caf9 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -60,6 +60,7 @@ "dependencies": { "@aws-cdk/cdk": "^0.10.0", "@aws-cdk/aws-autoscaling": "^0.10.0", + "@aws-cdk/aws-cloudwatch": "^0.10.0", "@aws-cdk/aws-ec2": "^0.10.0", "@aws-cdk/aws-elasticloadbalancing": "^0.10.0", "@aws-cdk/aws-elasticloadbalancingv2": "^0.10.0", From dca69727b56f7ce90bfcfd0e15ba7b529c7c7436 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 5 Oct 2018 18:12:33 +0200 Subject: [PATCH 081/140] Bridge mode works with host port specified --- packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts index 52e548daf620b..b28ee603455a7 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts @@ -23,6 +23,7 @@ const container = taskDefinition.addContainer('web', { }); container.addPortMappings({ containerPort: 80, + hostPort: 8080, protocol: ecs.Protocol.Tcp }); From ef51786019e10ef19ec0b59146794be9b0fe7050 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 12 Oct 2018 12:15:32 +0200 Subject: [PATCH 082/140] Fix libs to be in line with latest API changes from master --- packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts | 6 +++--- packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 3 ++- .../@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index 4c71d62a2a0d5..d56e17cb3c61d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -64,7 +64,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { this.executionRole = props.executionRole; this.taskRole = props.taskRole || new iam.Role(this, 'TaskRole', { - assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), }); const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { @@ -81,7 +81,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { /** * Add a policy statement to the Task Role */ - public addToRolePolicy(statement: cdk.PolicyStatement) { + public addToRolePolicy(statement: iam.PolicyStatement) { this.taskRole.addToPolicy(statement); } @@ -112,7 +112,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { private generateExecutionRole() { if (!this.executionRole) { this.executionRole = new iam.Role(this, 'ExecutionRole', { - assumedBy: new cdk.ServicePrincipal('ecs-tasks.amazonaws.com'), + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), }); this.executionRole.attachManagedPolicy(new iam.AwsManagedPolicy("service-role/AmazonECSTaskExecutionRolePolicy").policyArn); } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index 324d02c123221..aea6ba2d3b53e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -1,6 +1,7 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; @@ -67,7 +68,7 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { // ECS instances must be able to do these things // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html - autoScalingGroup.addToRolePolicy(new cdk.PolicyStatement().addActions( + autoScalingGroup.addToRolePolicy(new iam.PolicyStatement().addActions( "ecs:CreateCluster", "ecs:DeregisterContainerInstance", "ecs:DiscoverPollEndpoint", diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index 4b7cb5f6ca163..87ed17d1bab93 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -3,7 +3,7 @@ import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); import ecs = require('../../lib'); -const app = new cdk.App(process.argv); +const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); @@ -38,4 +38,4 @@ listener.addTargets('Fargate', { new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); -process.stdout.write(app.run()); \ No newline at end of file +app.run(); From 64b6d066cb8528ab431f1d77d19d54ae9377d329 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 17 Oct 2018 10:51:54 +0200 Subject: [PATCH 083/140] Fix build errors --- examples/cdk-examples-typescript/hello-cdk-ecs/index.ts | 4 ++-- .../cdk-examples-typescript/hello-cdk-fargate/index.ts | 4 ++-- packages/@aws-cdk/aws-applicationautoscaling/package.json | 1 + packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts | 4 ++-- packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts | 4 ++-- .../lib/shared/base-target-group.ts | 7 ------- 6 files changed, 9 insertions(+), 15 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 0c1bc12c58fd3..c392b053ba36a 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -95,8 +95,8 @@ class BonjourECS extends cdk.Stack { } } -const app = new cdk.App(process.argv); +const app = new cdk.App(); new BonjourECS(app, 'Bonjour'); -process.stdout.write(app.run()); +app.run(); diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts index 03456cf32280f..2d545c1b3cb9a 100644 --- a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts @@ -22,8 +22,8 @@ class BonjourFargate extends cdk.Stack { } } -const app = new cdk.App(process.argv); +const app = new cdk.App(); new BonjourFargate(app, 'Bonjour'); -process.stdout.write(app.run()); \ No newline at end of file +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index 6225e611434ae..9be6e16a0a485 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -59,6 +59,7 @@ }, "dependencies": { "@aws-cdk/cdk": "^0.12.0", + "@aws-cdk/aws-iam": "^0.12.0", "@aws-cdk/aws-cloudwatch": "^0.12.0" }, "homepage": "https://github.com/awslabs/aws-cdk" diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts index a81d9c8932999..205372b0e36ad 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts @@ -4,7 +4,7 @@ import cdk = require('@aws-cdk/cdk'); import ecs = require('../../lib'); import { NetworkMode } from '../../lib'; -const app = new cdk.App(process.argv); +const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); @@ -39,4 +39,4 @@ listener.addTargets('ECS', { new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); -process.stdout.write(app.run()); \ No newline at end of file +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts index b28ee603455a7..dbd8c8e798c30 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts @@ -4,7 +4,7 @@ import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); import ecs = require('../../lib'); -const app = new cdk.App(process.argv); +const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-ecs-integ-ecs'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); @@ -41,4 +41,4 @@ listener.addTargets('ECS', { new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); -process.stdout.write(app.run()); \ No newline at end of file +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 37d57290cd54f..265b65c934aa8 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -254,13 +254,6 @@ export abstract class BaseTargetGroup extends cdk.Construct implements ITargetGr return new LazyDependable(this.dependableListeners); } - /** - * Return an object to depend on the listeners added to this target group - */ - public listenerDependency(): cdk.IDependable { - return new LazyDependable(this.dependableListeners); - } - /** * Register the given load balancing target as part of this group */ From 51097fde6a5cf8fa709cc5908668e78831720430 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Sun, 21 Oct 2018 20:30:25 -0700 Subject: [PATCH 084/140] Fix ingressPort/containerPort tests --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 9 ++++++--- .../@aws-cdk/aws-ecs/lib/container-definition.ts | 5 ++++- .../aws-ecs/test/test.container-definition.ts | 16 ++++++++-------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 978fd6bf74619..81ef233c2d12e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -1,4 +1,4 @@ -import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); @@ -57,7 +57,7 @@ export interface BaseServiceProps { } export abstract class BaseService extends cdk.Construct - implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { + implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { public readonly dependencyElements: cdk.IDependable[]; public abstract readonly connections: ec2.Connections; public readonly serviceName: string; @@ -88,6 +88,9 @@ export abstract class BaseService extends cdk.Construct this.serviceName = this.resource.serviceName; } + /** + * FIXME How to reconcile this with the fact ECS registers service with target group automatically? + */ public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { const ret = this.attachToELBv2(targetGroup); @@ -133,7 +136,7 @@ export abstract class BaseService extends cdk.Construct this.networkConfiguration = { awsvpcConfiguration: { - assignPublicIp : assignPublicIp ? 'ENABLED' : 'DISABLED', + assignPublicIp: assignPublicIp ? 'ENABLED' : 'DISABLED', subnets: subnets.map(x => x.subnetId), securityGroups: new cdk.Token(() => [securityGroup!.securityGroupId]), } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 9d8f563fd4d5c..eac5e4935a8d5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -242,6 +242,9 @@ export class ContainerDefinition extends cdk.Construct { return this._usesEcrImages; } + /** + * Ingress Port is needed to set the security group ingress for the task/service. + */ public get ingressPort(): number { if (this.portMappings.length === 0) { throw new Error(`Container ${this.id} hasn't defined any ports`); @@ -258,7 +261,7 @@ export class ContainerDefinition extends cdk.Construct { return defaultPortMapping.containerPort; } /** - * Return the instance port that the container will be listening on + * Return the port that the container will be listening on by default */ public get containerPort(): number { if (this.portMappings.length === 0) { diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index d91ec616f9c3c..afd6ab836f168 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -123,7 +123,7 @@ export = { container.addPortMappings({ containerPort: 8080, }); - const actual = container.containerPort; + const actual = container.ingressPort; // THEN const expected = 8080; @@ -148,7 +148,7 @@ export = { container.addPortMappings({ containerPort: 8080, }); - const actual = container.containerPort; + const actual = container.ingressPort; // THEN const expected = 8080; @@ -171,17 +171,17 @@ export = { // WHEN container.addPortMappings({ - containerPort: 8081, - hostPort: 8080, + containerPort: 8080, + hostPort: 8081, }); - const actual = container.containerPort; + const actual = container.ingressPort; // THEN - const expected = 8080; + const expected = 8081; test.equal(actual, expected); test.done(); }, - "Ingress port should be the 0 if not supplied"(test: Test) { + "Ingress port should be 0 if not supplied"(test: Test) { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { @@ -197,7 +197,7 @@ export = { container.addPortMappings({ containerPort: 8081, }); - const actual = container.containerPort; + const actual = container.ingressPort; // THEN const expected = 0; From a8f50e49183e15b3329a9825dabed58d42f2e4cf Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 18 Oct 2018 16:10:54 +0200 Subject: [PATCH 085/140] Add AssetImage: Docker image built from source Also in this commit: - ContainerImage changed to an interface, because AssetImage needs to be a construct but DockerHubImage doesn't. - ECR containers now don't add the general managed ECS policy, but add fine-grained read permissions to the correct repository (requested by AWS security). - Add validation to ECS TaskDefinitions (to make it impossible to forget specifying memory limits in ContainerDefs). - Add LoadBalancedEcsService. --- packages/@aws-cdk/assets/lib/asset.ts | 2 +- .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 16 +- packages/@aws-cdk/aws-ecs/lib/asset-image.ts | 232 +++++++ .../aws-ecs/lib/base/base-task-definition.ts | 28 +- .../aws-ecs/lib/container-definition.ts | 23 +- .../@aws-cdk/aws-ecs/lib/container-image.ts | 14 +- .../aws-ecs/lib/ecs/ecs-task-definition.ts | 12 + packages/@aws-cdk/aws-ecs/lib/index.ts | 2 + .../aws-ecs/lib/load-balanced-ecs-service.ts | 97 +++ .../lib/load-balanced-fargate-service.ts | 4 +- packages/@aws-cdk/aws-ecs/package.json | 4 + .../aws-ecs/test/demo-image/Dockerfile | 5 + .../@aws-cdk/aws-ecs/test/demo-image/index.py | 33 + .../aws-ecs/test/ecs/integ.asset-image.ts | 22 + .../@aws-cdk/aws-ecs/test/fargate/cdk.json | 8 + .../aws-ecs/test/fargate/integ.asset-image.ts | 27 + .../test/integ.asset-image.expected.json | 620 ++++++++++++++++++ packages/@aws-cdk/cx-api/lib/cxapi.ts | 42 +- packages/aws-cdk/lib/api/toolkit-info.ts | 96 ++- packages/aws-cdk/lib/api/util/sdk.ts | 7 + packages/aws-cdk/lib/assets.ts | 13 +- packages/aws-cdk/lib/docker.ts | 208 ++++++ packages/aws-cdk/lib/logging.ts | 2 +- packages/aws-cdk/lib/os.ts | 98 +++ packages/aws-cdk/lib/util/please-hold.ts | 25 + 25 files changed, 1585 insertions(+), 55 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/asset-image.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts create mode 100644 packages/@aws-cdk/aws-ecs/test/demo-image/Dockerfile create mode 100644 packages/@aws-cdk/aws-ecs/test/demo-image/index.py create mode 100644 packages/@aws-cdk/aws-ecs/test/ecs/integ.asset-image.ts create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.ts create mode 100644 packages/@aws-cdk/aws-ecs/test/integ.asset-image.expected.json create mode 100644 packages/aws-cdk/lib/docker.ts create mode 100644 packages/aws-cdk/lib/os.ts create mode 100644 packages/aws-cdk/lib/util/please-hold.ts diff --git a/packages/@aws-cdk/assets/lib/asset.ts b/packages/@aws-cdk/assets/lib/asset.ts index ffef5ebcb3cef..ba26f3a737269 100644 --- a/packages/@aws-cdk/assets/lib/asset.ts +++ b/packages/@aws-cdk/assets/lib/asset.ts @@ -124,7 +124,7 @@ export class Asset extends cdk.Construct { // for tooling to be able to package and upload a directory to the // s3 bucket and plug in the bucket name and key in the correct // parameters. - const asset: cxapi.AssetMetadataEntry = { + const asset: cxapi.FileAssetMetadataEntry = { path: this.assetPath, id: this.uniqueId, packaging: props.packaging, diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index baaaf34d4a58f..9b5985d38aaed 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -49,7 +49,7 @@ export abstract class RepositoryRef extends cdk.Construct { /** * Refer to a particular image tag from this repository */ - public getImage(tag: string = "latest"): ecs.ContainerImage { + public getImage(tag: string = "latest"): ecs.IContainerImage { return new EcrImage(this, tag); } } @@ -76,15 +76,23 @@ class ImportedRepository extends RepositoryRef { } } -class EcrImage extends ecs.ContainerImage { +class EcrImage implements ecs.IContainerImage { public readonly imageName: string; + private readonly repositoryArn: string; constructor(repository: RepositoryRef, tag: string) { - super(); this.imageName = `${repository.repositoryUri}:${tag}`; + this.repositoryArn = repository.repositoryArn; } public bind(containerDefinition: ecs.ContainerDefinition): void { - containerDefinition.useEcrImage(); + // This image will be in ECR, so we need appropriate permissions. + containerDefinition.addToExecutionPolicy(new iam.PolicyStatement() + .addActions("ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage") + .addResource(this.repositoryArn)); + + containerDefinition.addToExecutionPolicy(new iam.PolicyStatement() + .addActions("ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents") + .addAllResources()); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/asset-image.ts b/packages/@aws-cdk/aws-ecs/lib/asset-image.ts new file mode 100644 index 0000000000000..0bbdd44de95ae --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/asset-image.ts @@ -0,0 +1,232 @@ +import cfn = require('@aws-cdk/aws-cloudformation'); +import iam = require('@aws-cdk/aws-iam'); +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import cxapi = require('@aws-cdk/cx-api'); +import fs = require('fs'); +import path = require('path'); +import { ContainerDefinition } from './container-definition'; +import { IContainerImage } from './container-image'; + +export interface AssetImageProps { + /** + * The directory where the Dockerfile is stored + */ + directory: string; +} + +/** + * An image that will be built at deployment time + */ +export class AssetImage extends cdk.Construct implements IContainerImage { + /** + * Full name of this image + */ + public readonly imageName: string; + + /** + * Directory where the source files are stored + */ + private readonly directory: string; + + /** + * ARN of the repository + */ + private readonly repositoryArn: string; + + constructor(parent: cdk.Construct, id: string, props: AssetImageProps) { + super(parent, id); + + // resolve full path + this.directory = path.resolve(props.directory); + if (!fs.existsSync(this.directory)) { + throw new Error(`Cannot find image directory at ${this.directory}`); + } + + const repositoryParameter = new cdk.Parameter(this, 'Repository', { + type: 'String', + description: `Repository ARN for asset "${this.path}"`, + }); + + const tagParameter = new cdk.Parameter(this, 'Tag', { + type: 'String', + description: `Tag for asset "${this.path}"`, + }); + + const asset: cxapi.ContainerImageAssetMetadataEntry = { + packaging: 'container-image', + path: this.directory, + id: this.uniqueId, + repositoryParameter: repositoryParameter.logicalId, + tagParameter: tagParameter.logicalId + }; + + this.addMetadata(cxapi.ASSET_METADATA, asset); + + this.repositoryArn = repositoryParameter.value.toString(); + + // Require that repository adoption happens first + const adopted = new AdoptRegistry(this, 'AdoptRegistry', { repositoryArn: this.repositoryArn }); + this.imageName = `${adopted.repositoryUri}:${tagParameter.value}`; + } + + public bind(containerDefinition: ContainerDefinition): void { + // This image will be in ECR, so we need appropriate permissions. + containerDefinition.addToExecutionPolicy(new iam.PolicyStatement() + .addActions("ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage") + .addResource(this.repositoryArn)); + + containerDefinition.addToExecutionPolicy(new iam.PolicyStatement() + .addActions("ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents") + .addAllResources()); + } +} + +interface AdoptRegistryProps { + repositoryArn: string; +} + +/** + * Custom Resource which will adopt the registry used for the locally built image into the stack. + * + * This is so we can clean it up when the stack gets deleted. + */ +class AdoptRegistry extends cdk.Construct { + public readonly repositoryUri: string; + + constructor(parent: cdk.Construct, id: string, props: AdoptRegistryProps) { + super(parent, id); + + const fn = new lambda.SingletonFunction(this, 'Function', { + runtime: lambda.Runtime.NodeJS810, + lambdaPurpose: 'AdoptEcrRegistry', + handler: 'index.handler', + code: lambda.Code.inline(`exports.handler = ${trivialMinify(adoptRegistryHandler.toString())}`), + uuid: 'dbc60def-c595-44bc-aa5c-28c95d68f62c', + timeout: 300 + }); + + fn.addToRolePolicy(new iam.PolicyStatement() + .addActions('ecr:GetRepositoryPolicy', 'ecr:SetRepositoryPolicy', 'ecr:DeleteRepository', 'ecr:ListImages', 'ecr:BatchDeleteImage') + .addAllResources()); + + const resource = new cfn.CustomResource(this, 'Resource', { + lambdaProvider: fn, + properties: { + RepositoryArn: props.repositoryArn, + } + }); + + this.repositoryUri = resource.getAtt('RepositoryUri').toString(); + } +} + +// tslint:disable:no-console +async function adoptRegistryHandler(event: any, context: any) { + try { + const AWS = require('aws-sdk'); + const ecr = new AWS.ECR(); + + console.log(JSON.stringify(event)); + + const markerStatement = { + Sid: event.StackId, + Effect: "Deny", + Action: "OwnedBy:CDKStack", + Principal: "*" + }; + + function repoName(props: any) { + return props.RepositoryArn.split('/').slice(1).join('/'); + } + + // The repository must already exist + async function getAdopter(name: string): Promise { + try { + const policyResponse = await ecr.getRepositoryPolicy({ repositoryName: name }).promise(); + const policy = JSON.parse(policyResponse.policyText); + // Search the policy for an adopter marker + return (policy.Statement || []).find((x: any) => x.Action === markerStatement.Action) || {}; + } catch (e) { + if (e.code !== 'RepositoryPolicyNotFoundException') { throw e; } + return {}; + } + } + + const repo = repoName(event.ResourceProperties); + const adopter = await getAdopter(repo); + if (event.RequestType === 'Delete') { + if (adopter.Sid !== markerStatement.Sid) { + throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); + } + console.log('Deleting', repo); + const ids = (await ecr.listImages({ repositoryName: repo }).promise()).imageIds; + try { + await ecr.batchDeleteImage({ repositoryName: repo, imageIds: ids }).promise(); + await ecr.deleteRepository({ repositoryName: repo }).promise(); + } catch (e) { + if (e.code !== 'RepositoryPolicyNotFoundException') { throw e; } + } + } + + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + if (adopter.Sid !== undefined && adopter.Sid !== markerStatement.Sid) { + throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); + } + console.log('Adopting', repo); + await ecr.setRepositoryPolicy({ repositoryName: repo, policyText: JSON.stringify({ + Version: '2008-10-17', + Statement: [markerStatement] + }) }).promise(); + } + + const arn = event.ResourceProperties.RepositoryArn.split(':'); + await respond("SUCCESS", "OK", repo, { + RepositoryUri: `${arn[4]}.dkr.ecr.${arn[3]}.amazonaws.com/${repoName(event.ResourceProperties)}` + }); + } catch (e) { + console.log(e); + await respond("FAILED", e.message, context.logStreamName, {}); + } + + function respond(responseStatus: string, reason: string, physId: string, data: any) { + const responseBody = JSON.stringify({ + Status: responseStatus, + Reason: reason, + PhysicalResourceId: physId, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + NoEcho: false, + Data: data + }); + + console.log('Responding', JSON.stringify(responseBody)); + + const parsedUrl = require('url').parse(event.ResponseURL); + const requestOptions = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: "PUT", + headers: { "content-type": "", "content-length": responseBody.length } + }; + + return new Promise((resolve, reject) => { + try { + const request = require('https').request(requestOptions, resolve); + request.on("error", reject); + request.write(responseBody); + request.end(); + } catch (e) { + reject(e); + } + }); + } +} + +/** + * Trivial minification by changing TypeScript's 4-space indentation to 1-space + */ +function trivialMinify(s: string) { + return s.replace(/^ {4}/mg, ' '); +} diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index d56e17cb3c61d..8114188105564 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -48,7 +48,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { * container. */ public defaultContainer?: ContainerDefinition; - private readonly containers = new Array(); + protected readonly containers = new Array(); private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; private executionRole?: iam.Role; @@ -81,19 +81,25 @@ export abstract class BaseTaskDefinition extends cdk.Construct { /** * Add a policy statement to the Task Role */ - public addToRolePolicy(statement: iam.PolicyStatement) { + public addToTaskRolePolicy(statement: iam.PolicyStatement) { this.taskRole.addToPolicy(statement); } + public addToExecutionRolePolicy(statement: iam.PolicyStatement) { + if (!this.executionRole) { + this.executionRole = new iam.Role(this, 'ExecutionRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + } + this.executionRole.addToPolicy(statement); + } + /** * Add a container to this task */ public addContainer(id: string, props: ContainerDefinitionProps) { const container = new ContainerDefinition(this, id, this, props); this.containers.push(container); - if (container.usesEcrImages) { - this.generateExecutionRole(); - } if (this.defaultContainer === undefined && container.essential) { this.defaultContainer = container; } @@ -105,18 +111,6 @@ export abstract class BaseTaskDefinition extends cdk.Construct { // const v = this.renderVolume(volume); this.volumes.push(volume); } - - /** - * Generate a default execution role that allows pulling from ECR - */ - private generateExecutionRole() { - if (!this.executionRole) { - this.executionRole = new iam.Role(this, 'ExecutionRole', { - assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), - }); - this.executionRole.attachManagedPolicy(new iam.AwsManagedPolicy("service-role/AmazonECSTaskExecutionRolePolicy").policyArn); - } - } } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index eac5e4935a8d5..15eb15a20d3e2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -1,6 +1,7 @@ +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { BaseTaskDefinition, NetworkMode } from './base/base-task-definition'; -import { ContainerImage } from './container-image'; +import { IContainerImage } from './container-image'; import { cloudformation } from './ecs.generated'; import { LinuxParameters } from './linux-parameters'; import { LogDriver } from './log-drivers/log-driver'; @@ -11,9 +12,9 @@ export interface ContainerDefinitionProps { * * You can use images in the Docker Hub registry or specify other * repositories (repository-url/image:tag). - * TODO: Update these to specify using classes of ContainerImage + * TODO: Update these to specify using classes of IContainerImage */ - image: ContainerImage; + image: IContainerImage; /** * The CMD value to pass to the container. @@ -182,16 +183,18 @@ export class ContainerDefinition extends cdk.Construct { public readonly essential: boolean; + public readonly memoryLimitSpecified: boolean; + private readonly links = new Array(); private readonly taskDefinition: BaseTaskDefinition; - private _usesEcrImages: boolean = false; - constructor(parent: cdk.Construct, id: string, taskDefinition: BaseTaskDefinition, private readonly props: ContainerDefinitionProps) { super(parent, id); this.essential = props.essential !== undefined ? props.essential : true; this.taskDefinition = taskDefinition; + this.memoryLimitSpecified = props.memoryLimitMiB !== undefined || props.memoryReservationMiB !== undefined; + props.image.bind(this); } @@ -232,14 +235,10 @@ export class ContainerDefinition extends cdk.Construct { } /** - * Mark this ContainerDefinition as using an ECR image + * Add a statement to the Task Definition's Execution policy */ - public useEcrImage() { - this._usesEcrImages = true; - } - - public get usesEcrImages() { - return this._usesEcrImages; + public addToExecutionPolicy(statement: iam.PolicyStatement) { + this.taskDefinition.addToExecutionRolePolicy(statement); } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/container-image.ts b/packages/@aws-cdk/aws-ecs/lib/container-image.ts index 25ecc9fb5590a..719e5817a416a 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-image.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-image.ts @@ -1,21 +1,21 @@ import { ContainerDefinition } from './container-definition'; -export abstract class ContainerImage { - public abstract readonly imageName: string; - public abstract bind(containerDefinition: ContainerDefinition): void; +export interface IContainerImage { + readonly imageName: string; + bind(containerDefinition: ContainerDefinition): void; } export class DockerHub { - public static image(name: string): ContainerImage { + public static image(name: string): IContainerImage { return new DockerHubImage(name); } } -class DockerHubImage { +class DockerHubImage implements IContainerImage { constructor(public readonly imageName: string) { } public bind(_containerDefinition: ContainerDefinition): void { - // Nothing + // Nothing to do } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts index 19659836781d9..21ee524667f80 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts @@ -41,6 +41,18 @@ export class EcsTaskDefinition extends BaseTaskDefinition { } } + public validate(): string[] { + const ret = super.validate(); + + for (const container of this.containers) { + if (!container.memoryLimitSpecified) { + ret.push(`ECS Container ${container.id} must have at least one of 'memoryLimitMiB' or 'memoryReservationMiB' specified`); + } + } + + return ret; + } + /** * Constrain where tasks can be placed */ diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 0c06c5155f709..b71125235ec3f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -13,7 +13,9 @@ export * from './container-definition'; export * from './container-image'; export * from './linux-parameters'; export * from './load-balanced-fargate-service'; +export * from './load-balanced-ecs-service'; export * from './load-balanced-fargate-service-applet'; +export * from './asset-image'; export * from './log-drivers/log-driver'; export * from './log-drivers/aws-log-driver'; diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts new file mode 100644 index 0000000000000..fe560a09d9b04 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts @@ -0,0 +1,97 @@ +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import { IContainerImage } from './container-image'; +import { IEcsCluster } from './ecs/ecs-cluster'; +import { EcsService } from './ecs/ecs-service'; +import { EcsTaskDefinition } from './ecs/ecs-task-definition'; + +export interface LoadBalancedEcsServiceProps { + /** + * The cluster where your ECS service will be deployed + */ + cluster: IEcsCluster; + + image: IContainerImage; + + /** + * The number of cpu units used by the task. + * Valid values, which determines your range of valid values for the memory parameter: + * 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB + * 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB + * 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB + * 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments + * 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments + * + * This default is set in the underlying EcsTaskDefinition construct. + * + * @default 256 + */ + cpu?: string; + + /** + * Hard limit on memory + */ + memoryLimitMiB?: number; + + /** + * Memory reservation + */ + memoryReservationMiB?: number; + + /** + * The container port of the application load balancer attached to your Ecs service. Corresponds to container port mapping. + * + * @default 80 + */ + containerPort?: number; + + /** + * Determines whether the Application Load Balancer will be internet-facing + * + * @default true + */ + publicLoadBalancer?: boolean; +} + +export class LoadBalancedEcsService extends cdk.Construct { + public readonly loadBalancer: elbv2.ApplicationLoadBalancer; + + constructor(parent: cdk.Construct, id: string, props: LoadBalancedEcsServiceProps) { + super(parent, id); + + const taskDefinition = new EcsTaskDefinition(this, 'TaskDef', { + }); + + const container = taskDefinition.addContainer('web', { + image: props.image, + memoryLimitMiB: props.memoryLimitMiB, + memoryReservationMiB: props.memoryReservationMiB, + }); + + container.addPortMappings({ + containerPort: props.containerPort || 80, + }); + + const service = new EcsService(this, "Service", { + cluster: props.cluster, + taskDefinition, + }); + + const internetFacing = props.publicLoadBalancer !== undefined ? props.publicLoadBalancer : true; + const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { + vpc: props.cluster.vpc, + internetFacing + }); + + this.loadBalancer = lb; + + const listener = lb.addListener('Listener', { port: 80, open: true }); + listener.addTargets('ECS', { + port: 80, + targets: [service] + }); + + // Always output load balancer address, because why not? + new cdk.Output(this, 'LoadBalancerDNS', { value: lb.dnsName }); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts index 04cae422cc05d..8063d2b10aa41 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts @@ -1,6 +1,6 @@ import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); -import { ContainerImage } from './container-image'; +import { IContainerImage } from './container-image'; import { IFargateCluster } from './fargate/fargate-cluster'; import { FargateService } from './fargate/fargate-service'; import { FargateTaskDefinition } from './fargate/fargate-task-definition'; @@ -11,7 +11,7 @@ export interface LoadBalancedFargateServiceProps { */ cluster: IFargateCluster; - image: ContainerImage; + image: IContainerImage; /** * The number of cpu units used by the task. * Valid values, which determines your range of valid values for the memory parameter: diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index d0dd8ce35b819..5f8a3a5ccba6c 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -54,13 +54,17 @@ "devDependencies": { "@aws-cdk/assert": "^0.12.0", "cdk-build-tools": "^0.12.0", + "cdk-integ-tools": "^0.12.0", "cfn2ts": "^0.12.0", "pkglint": "^0.12.0" }, "dependencies": { "@aws-cdk/cdk": "^0.12.0", + "@aws-cdk/cx-api": "^0.12.0", "@aws-cdk/aws-autoscaling": "^0.12.0", "@aws-cdk/aws-cloudwatch": "^0.12.0", + "@aws-cdk/aws-lambda": "^0.12.0", + "@aws-cdk/aws-cloudformation": "^0.12.0", "@aws-cdk/aws-ec2": "^0.12.0", "@aws-cdk/aws-elasticloadbalancing": "^0.12.0", "@aws-cdk/aws-elasticloadbalancingv2": "^0.12.0", diff --git a/packages/@aws-cdk/aws-ecs/test/demo-image/Dockerfile b/packages/@aws-cdk/aws-ecs/test/demo-image/Dockerfile new file mode 100644 index 0000000000000..123b5670febc8 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/demo-image/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.6 +EXPOSE 8000 +WORKDIR /src +ADD . /src +CMD python3 index.py diff --git a/packages/@aws-cdk/aws-ecs/test/demo-image/index.py b/packages/@aws-cdk/aws-ecs/test/demo-image/index.py new file mode 100644 index 0000000000000..2ccedfce3ab76 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/demo-image/index.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +import sys +import textwrap +import http.server +import socketserver + +PORT = 8000 + + +class Handler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-Type', 'text/html') + self.end_headers() + self.wfile.write(textwrap.dedent('''\ + + It works + +

Hello from the integ test container

+

This container got built and started as part of the integ test.

+ + + ''').encode('utf-8')) + + +def main(): + httpd = http.server.HTTPServer(("", PORT), Handler) + print("serving at port", PORT) + httpd.serve_forever() + + +if __name__ == '__main__': + main() diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/ecs/integ.asset-image.ts new file mode 100644 index 0000000000000..5ad970a0ce4a8 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/integ.asset-image.ts @@ -0,0 +1,22 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import path = require('path'); +import ecs = require('../../lib'); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-integ2'); +const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + +const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); + +// Instantiate Ecs Service with just cluster and image +new ecs.LoadBalancedEcsService(stack, "EcsService", { + cluster, + containerPort: 8000, + memoryReservationMiB: 128, + image: new ecs.AssetImage(stack, 'Image', { + directory: path.join(__dirname, '..', 'demo-image') + }) +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json index 64fd3b2dafe6e..7726abccdd2ce 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json @@ -4,6 +4,14 @@ "eu-west-1a", "eu-west-1b", "eu-west-1c" + ], + "availability-zones:993655754359:us-east-1": [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f" ] } } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.ts new file mode 100644 index 0000000000000..4366cf2ae6722 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.ts @@ -0,0 +1,27 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import path = require('path'); +import ecs = require('../../lib'); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); +const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + +const cluster = new ecs.FargateCluster(stack, 'Cluster', { vpc }); + +Array.isArray(cluster); +Array.isArray(path); + +// Instantiate Fargate Service with just cluster and image +const fargateService = new ecs.LoadBalancedFargateService(stack, "FargateService", { + cluster, + containerPort: 8000, + image: new ecs.AssetImage(stack, 'Image', { + directory: path.join(__dirname, '..', 'demo-image') + }) +}); + +// Output the DNS where you can access your service +new cdk.Output(stack, 'LoadBalancerDNS', { value: fargateService.loadBalancer.dnsName }); + +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/integ.asset-image.expected.json b/packages/@aws-cdk/aws-ecs/test/integ.asset-image.expected.json new file mode 100644 index 0000000000000..34fdfd688c81a --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/integ.asset-image.expected.json @@ -0,0 +1,620 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "ClusterEB0386A7": { + "Type": "AWS::ECS::Cluster" + }, + "FargateServiceTaskDefTaskRole8CDCF85E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ] + } + } + }, + "FargateServiceTaskDef940E3A80": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": { + "Fn::Join": [ + "", + [ + { + "Ref": "ImageRepositoryC2BE7AD4" + }, + ":", + { + "Ref": "ImageTagE17D8A6B" + } + ] + ] + }, + "Links": [], + "LinuxParameters": { + "Capabilities": { + "Add": [], + "Drop": [] + }, + "Devices": [], + "Tmpfs": [] + }, + "MountPoints": [], + "Name": "web", + "PortMappings": [ + { + "ContainerPort": 8000, + "Protocol": "tcp" + } + ], + "Ulimits": [], + "VolumesFrom": [] + } + ], + "Cpu": "256", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "FargateServiceTaskDefExecutionRole9194820E", + "Arn" + ] + }, + "Family": "awsecsintegFargateServiceTaskDefE1C73F14", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "FargateServiceTaskDefTaskRole8CDCF85E", + "Arn" + ] + } + } + }, + "FargateServiceTaskDefExecutionRole9194820E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + ] + ] + } + ] + } + }, + "FargateServiceECC8084D": { + "Type": "AWS::ECS::Service", + "Properties": { + "TaskDefinition": { + "Ref": "FargateServiceTaskDef940E3A80" + }, + "Cluster": { + "Ref": "ClusterEB0386A7" + }, + "DeploymentConfiguration": {}, + "DesiredCount": 1, + "LaunchType": "FARGATE", + "LoadBalancers": [ + { + "ContainerName": "web", + "ContainerPort": 8000, + "TargetGroupArn": { + "Ref": "FargateServiceLBPublicListenerECSGroupBE57E081" + } + } + ], + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FargateServiceSecurityGroup262B61DD", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + } + } + }, + "DependsOn": [ + "FargateServiceLBPublicListener4B4929CA" + ] + }, + "FargateServiceSecurityGroup262B61DD": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ/FargateService/Service/SecurityGroup", + "SecurityGroupEgress": [], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "FargateServiceSecurityGroupfromawsecsintegFargateServiceLBSecurityGroup129467A18000AD32AE25": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "FromPort": 8000, + "GroupId": { + "Fn::GetAtt": [ + "FargateServiceSecurityGroup262B61DD", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "FargateServiceLBSecurityGroup5F444C78", + "GroupId" + ] + }, + "ToPort": 8000 + } + }, + "FargateServiceLBB353E155": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "LoadBalancerAttributes": [], + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FargateServiceLBSecurityGroup5F444C78", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + ], + "Type": "application" + } + }, + "FargateServiceLBSecurityGroup5F444C78": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB awsecsintegFargateServiceLB5FE4725D", + "SecurityGroupEgress": [], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow from anyone on port 80", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "FargateServiceLBSecurityGrouptoawsecsintegFargateServiceSecurityGroup8930AEB880001FF8BADE": { + "Type": "AWS::EC2::SecurityGroupEgress", + "Properties": { + "GroupId": { + "Fn::GetAtt": [ + "FargateServiceLBSecurityGroup5F444C78", + "GroupId" + ] + }, + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "FargateServiceSecurityGroup262B61DD", + "GroupId" + ] + }, + "FromPort": 8000, + "ToPort": 8000 + } + }, + "FargateServiceLBPublicListener4B4929CA": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "FargateServiceLBPublicListenerECSGroupBE57E081" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "FargateServiceLBB353E155" + }, + "Port": 80, + "Protocol": "HTTP", + "Certificates": [] + } + }, + "FargateServiceLBPublicListenerECSGroupBE57E081": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "TargetGroupAttributes": [], + "Targets": [], + "TargetType": "ip" + } + } + }, + "Parameters": { + "ImageRepositoryC2BE7AD4": { + "Type": "String", + "Description": "Repository for asset \"aws-ecs-integ/Image\"" + }, + "ImageTagE17D8A6B": { + "Type": "String", + "Description": "Tag for asset \"aws-ecs-integ/Image\"" + } + }, + "Outputs": { + "LoadBalancerDNS": { + "Value": { + "Fn::GetAtt": [ + "FargateServiceLBB353E155", + "DNSName" + ] + }, + "Export": { + "Name": "aws-ecs-integ:LoadBalancerDNS" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/cxapi.ts b/packages/@aws-cdk/cx-api/lib/cxapi.ts index 56a96d12855be..1ab3431299f9b 100644 --- a/packages/@aws-cdk/cx-api/lib/cxapi.ts +++ b/packages/@aws-cdk/cx-api/lib/cxapi.ts @@ -79,7 +79,13 @@ export const DEFAULT_ACCOUNT_CONTEXT_KEY = 'aws:cdk:toolkit:default-account'; export const DEFAULT_REGION_CONTEXT_KEY = 'aws:cdk:toolkit:default-region'; export const ASSET_METADATA = 'aws:cdk:asset'; -export interface AssetMetadataEntry { + +export interface FileAssetMetadataEntry { + /** + * Requested packaging style + */ + packaging: 'zip' | 'file'; + /** * Path on disk to the asset */ @@ -90,11 +96,6 @@ export interface AssetMetadataEntry { */ id: string; - /** - * Requested packaging style - */ - packaging: 'zip' | 'file'; - /** * Name of parameter where S3 bucket should be passed in */ @@ -106,6 +107,35 @@ export interface AssetMetadataEntry { s3KeyParameter: string; } +export interface ContainerImageAssetMetadataEntry { + /** + * Type of asset + */ + packaging: 'container-image'; + + /** + * Path on disk to the asset + */ + path: string; + + /** + * Logical identifier for the asset + */ + id: string; + + /** + * Name of the parameter that takes the repository name + */ + repositoryParameter: string; + + /** + * Name of the parameter that takes the tag + */ + tagParameter: string; +} + +export type AssetMetadataEntry = FileAssetMetadataEntry | ContainerImageAssetMetadataEntry; + /** * Metadata key used to print INFO-level messages by the toolkit when an app is syntheized. */ diff --git a/packages/aws-cdk/lib/api/toolkit-info.ts b/packages/aws-cdk/lib/api/toolkit-info.ts index 55465bd3747d6..5b19e66d20da0 100644 --- a/packages/aws-cdk/lib/api/toolkit-info.ts +++ b/packages/aws-cdk/lib/api/toolkit-info.ts @@ -21,12 +21,15 @@ export interface Uploaded { } export class ToolkitInfo { + public readonly sdk: SDK; + constructor(private readonly props: { sdk: SDK, bucketName: string, bucketEndpoint: string, environment: cxapi.Environment - }) { } + }) { + } public get bucketUrl() { return `https://${this.props.bucketEndpoint}`; @@ -73,6 +76,82 @@ export class ToolkitInfo { return { filename, key, changed: true }; } + /** + * Prepare an ECR repository for uploading to using Docker + */ + public async prepareEcrRepository(id: string, imageTag: string): Promise { + const ecr = await this.props.sdk.ecr(this.props.environment, Mode.ForWriting); + + // Create the repository if it doesn't exist yet + const repositoryName = 'cdk/' + id.replace(/[:/]/g, '-').toLowerCase(); + + let repository; + try { + const describeResponse = await ecr.describeRepositories({ repositoryNames: [repositoryName] }).promise(); + repository = describeResponse.repositories![0]; + } catch (e) { + if (e.code !== 'RepositoryNotFoundException') { throw e; } + } + + if (repository) { + try { + await ecr.describeImages({ repositoryName, imageIds: [{ imageTag }] }).promise(); + + // If we got here, the image already exists. Nothing else needs to be done. + return { + alreadyExists: true, + repositoryUri: repository.repositoryUri!, + repositoryArn: repository.repositoryArn!, + }; + } catch (e) { + if (e.code !== 'ImageNotFoundException') { throw e; } + } + } else { + const response = await ecr.createRepository({ repositoryName }).promise(); + repository = response.repository!; + + // Better put a lifecycle policy on this so as to not cost too much money + await ecr.putLifecyclePolicy({ + repositoryName, + lifecyclePolicyText: JSON.stringify(DEFAULT_REPO_LIFECYCLE) + }).promise(); + } + + // The repo exists, image just needs to be uploaded. Get auth to do so. + + const authData = (await ecr.getAuthorizationToken({ }).promise()).authorizationData || []; + if (authData.length === 0) { + throw new Error('No authorization data received from ECR'); + } + const token = Buffer.from(authData[0].authorizationToken!, 'base64').toString('ascii'); + const [username, password] = token.split(':'); + + return { + alreadyExists: false, + repositoryUri: repository.repositoryUri!, + repositoryArn: repository.repositoryArn!, + username, + password, + endpoint: authData[0].proxyEndpoint!, + }; + } +} + +export type EcrRepositoryInfo = CompleteEcrRepositoryInfo | UploadableEcrRepositoryInfo; + +export interface CompleteEcrRepositoryInfo { + repositoryUri: string; + repositoryArn: string; + alreadyExists: true; +} + +export interface UploadableEcrRepositoryInfo { + repositoryUri: string; + repositoryArn: string; + alreadyExists: false; + username: string; + password: string; + endpoint: string; } async function objectExists(s3: aws.S3, bucket: string, key: string) { @@ -114,3 +193,18 @@ function getOutputValue(stack: aws.CloudFormation.Stack, output: string): string } return result; } + +const DEFAULT_REPO_LIFECYCLE = { + rules: [ + { + rulePriority: 100, + description: 'Retain only 5 images', + selection: { + tagStatus: 'any', + countType: 'imageCountMoreThan', + countNumber: 5, + }, + action: { type: 'expire' } + } + ] +}; \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/util/sdk.ts b/packages/aws-cdk/lib/api/util/sdk.ts index 9b795e0ef1858..9c8ca43ac7177 100644 --- a/packages/aws-cdk/lib/api/util/sdk.ts +++ b/packages/aws-cdk/lib/api/util/sdk.ts @@ -104,6 +104,13 @@ export class SDK { }); } + public async ecr(environment: Environment, mode: Mode): Promise { + return new AWS.ECR({ + region: environment.region, + credentials: await this.credentialsCache.get(environment.account, mode) + }); + } + public async defaultRegion(): Promise { return await getCLICompatibleDefaultRegion(this.profile); } diff --git a/packages/aws-cdk/lib/assets.ts b/packages/aws-cdk/lib/assets.ts index a264b2a3eeee8..7a30f8500b5fe 100644 --- a/packages/aws-cdk/lib/assets.ts +++ b/packages/aws-cdk/lib/assets.ts @@ -1,10 +1,12 @@ -import { ASSET_METADATA, ASSET_PREFIX_SEPARATOR, AssetMetadataEntry, StackMetadata, SynthesizedStack } from '@aws-cdk/cx-api'; +// tslint:disable-next-line:max-line-length +import { ASSET_METADATA, ASSET_PREFIX_SEPARATOR, AssetMetadataEntry, FileAssetMetadataEntry, StackMetadata, SynthesizedStack } from '@aws-cdk/cx-api'; import { CloudFormation } from 'aws-sdk'; import fs = require('fs-extra'); import os = require('os'); import path = require('path'); import { ToolkitInfo } from './api/toolkit-info'; import { zipDirectory } from './archive'; +import { prepareContainerAsset } from './docker'; import { debug, success } from './logging'; export async function prepareAssets(stack: SynthesizedStack, toolkitInfo?: ToolkitInfo): Promise { @@ -35,12 +37,15 @@ async function prepareAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitInfo) return await prepareZipAsset(asset, toolkitInfo); case 'file': return await prepareFileAsset(asset, toolkitInfo); + case 'container-image': + return await prepareContainerAsset(asset, toolkitInfo); default: - throw new Error(`Unsupported packaging type: ${asset.packaging}`); + // tslint:disable-next-line:max-line-length + throw new Error(`Unsupported packaging type: ${(asset as any).packaging}. You might need to upgrade your aws-cdk toolkit to support this asset type.`); } } -async function prepareZipAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitInfo): Promise { +async function prepareZipAsset(asset: FileAssetMetadataEntry, toolkitInfo: ToolkitInfo): Promise { debug('Preparing zip asset from directory:', asset.path); const staging = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-assets')); try { @@ -60,7 +65,7 @@ async function prepareZipAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitIn * @param contentType Content-type to use when uploading to S3 (none will be specified by default) */ async function prepareFileAsset( - asset: AssetMetadataEntry, + asset: FileAssetMetadataEntry, toolkitInfo: ToolkitInfo, filePath?: string, contentType?: string): Promise { diff --git a/packages/aws-cdk/lib/docker.ts b/packages/aws-cdk/lib/docker.ts new file mode 100644 index 0000000000000..0fd0c3c54f6fa --- /dev/null +++ b/packages/aws-cdk/lib/docker.ts @@ -0,0 +1,208 @@ +import { ContainerImageAssetMetadataEntry } from '@aws-cdk/cx-api'; +import { CloudFormation } from 'aws-sdk'; +import crypto = require('crypto'); +import { ToolkitInfo } from './api/toolkit-info'; +import { debug, print } from './logging'; +import { shell } from './os'; +import { PleaseHold } from './util/please-hold'; + +/** + * Build and upload a Docker image + * + * Permanently identifying images is a bit of a bust. Newer Docker version use + * a digest (sha256:xxxx) as an image identifier, which is pretty good to avoid + * spurious rebuilds. However, this digest is calculated over a manifest that + * includes metadata that is liable to change. For example, as soon as we + * push the Docker image to a repository, the digest changes. This makes the + * digest worthless to determe whether we already pushed an image, for example. + * + * As a workaround, we calculate our own digest over parts of the manifest that + * are unlikely to change, and tag based on that. + */ +export async function prepareContainerAsset(asset: ContainerImageAssetMetadataEntry, toolkitInfo: ToolkitInfo): Promise { + debug(' 👑 Preparing Docker image asset:', asset.path); + + const buildHold = new PleaseHold(` ⌛ Building Docker image for ${asset.path}; this may take a while.`); + try { + buildHold.start(); + + const command = ['docker', + 'build', + '--quiet', + asset.path]; + const imageId = (await shell(command, { quiet: true })).trim(); + buildHold.stop(); + + const tag = await calculateImageDigest(imageId); + + debug(` ⌛ Image has tag ${tag}, preparing ECR repository`); + const ecr = await toolkitInfo.prepareEcrRepository(asset.id, tag); + + if (ecr.alreadyExists) { + debug(' 👑 Image already uploaded.'); + } else { + // Login and push + debug(` ⌛ Image needs to be uploaded first.`); + + await shell(['docker', 'login', + '--username', ecr.username, + '--password', ecr.password, + ecr.endpoint]); + + const qualifiedImageName = `${ecr.repositoryUri}:${tag}`; + await shell(['docker', 'tag', imageId, qualifiedImageName]); + + // There's no way to make this quiet, so we can't use a PleaseHold. Print a header message. + print(` ⌛ Pusing Docker image for ${asset.path}; this may take a while.`); + await shell(['docker', 'push', qualifiedImageName]); + } + + return [ + { ParameterKey: asset.repositoryParameter, ParameterValue: ecr.repositoryArn }, + { ParameterKey: asset.tagParameter, ParameterValue: tag }, + ]; + } catch (e) { + if (e.code === 'ENOENT') { + // tslint:disable-next-line:max-line-length + throw new Error('Error building Docker image asset; you need to have Docker installed in order to be able to build image assets. Please install Docker and try again.'); + } + throw e; + } finally { + buildHold.stop(); + } +} + +/** + * Calculate image digest + */ +async function calculateImageDigest(imageId: string) { + const manifest = await shell(['docker', 'inspect', imageId], { quiet: true }); + const parsed = JSON.parse(manifest)[0]; + + const importantParts = { + Parent: parsed.Parent, + Comment: parsed.Comment, + Config: parsed.Config, + RootFS: parsed.RootFS, + Architecture: parsed.Architecture, + Os: parsed.Os, + Size: parsed.Size, + VirtualSize: parsed.VirtualSize, + GraphDriver: parsed.GraphDriver + }; + + return crypto.createHash('sha256').update(JSON.stringify(importantParts)).digest('hex'); +} + +/** + * Example of a Docker manifest + * + * [ + * { + * "Id": "sha256:3a90542991d03007fd1d8f3b3a6ab04ebb02386785430fe48a867768a048d828", + * "RepoTags": [ + * "993655754359.dkr.ecr.us-east-1.amazonaws.com/cdk/awsecsintegimage7c15b8c6:latest" + * ], + * "RepoDigests": [ + * "993655754359.dkr.ecr.us-east-1.amazo....5e50c0cfc3f2355191934b05df68cd3339a044959111ffec2e14765" + * ], + * "Parent": "sha256:465720f8f43c9c0aff5dcc731d4e368a3927cae4e885442d4ba0bf8a867b7561", + * "Comment": "", + * "Created": "2018-10-17T10:16:40.775888476Z", + * "Container": "20f145d2e7fbf126ca9f4422497b932bc96b5faa038dc032de1e246f64e03a66", + * "ContainerConfig": { + * "Hostname": "9b48b580a312", + * "Domainname": "", + * "User": "", + * "AttachStdin": false, + * "AttachStdout": false, + * "AttachStderr": false, + * "ExposedPorts": { + * "8000/tcp": {} + * }, + * "Tty": false, + * "OpenStdin": false, + * "StdinOnce": false, + * "Env": [ + * "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + * "LANG=C.UTF-8", + * "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D", + * "PYTHON_VERSION=3.6.6", + * "PYTHON_PIP_VERSION=18.1" + * ], + * "Cmd": [ + * "/bin/sh", + * "-c", + * "#(nop) ", + * "CMD [\"/bin/sh\" \"-c\" \"python3 index.py\"]" + * ], + * "ArgsEscaped": true, + * "Image": "sha256:465720f8f43c9c0aff5dcc731d4e368a3927cae4e885442d4ba0bf8a867b7561", + * "Volumes": null, + * "WorkingDir": "/code", + * "Entrypoint": null, + * "OnBuild": [], + * "Labels": {} + * }, + * "DockerVersion": "17.03.2-ce", + * "Author": "", + * "Config": { + * "Hostname": "9b48b580a312", + * "Domainname": "", + * "User": "", + * "AttachStdin": false, + * "AttachStdout": false, + * "AttachStderr": false, + * "ExposedPorts": { + * "8000/tcp": {} + * }, + * "Tty": false, + * "OpenStdin": false, + * "StdinOnce": false, + * "Env": [ + * "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + * "LANG=C.UTF-8", + * "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D", + * "PYTHON_VERSION=3.6.6", + * "PYTHON_PIP_VERSION=18.1" + * ], + * "Cmd": [ + * "/bin/sh", + * "-c", + * "python3 index.py" + * ], + * "ArgsEscaped": true, + * "Image": "sha256:465720f8f43c9c0aff5dcc731d4e368a3927cae4e885442d4ba0bf8a867b7561", + * "Volumes": null, + * "WorkingDir": "/code", + * "Entrypoint": null, + * "OnBuild": [], + * "Labels": {} + * }, + * "Architecture": "amd64", + * "Os": "linux", + * "Size": 917730468, + * "VirtualSize": 917730468, + * "GraphDriver": { + * "Name": "aufs", + * "Data": null + * }, + * "RootFS": { + * "Type": "layers", + * "Layers": [ + * "sha256:f715ed19c28b66943ac8bc12dbfb828e8394de2530bbaf1ecce906e748e4fdff", + * "sha256:8bb25f9cdc41e7d085033af15a522973b44086d6eedd24c11cc61c9232324f77", + * "sha256:08a01612ffca33483a1847c909836610610ce523fb7e1aca880140ee84df23e9", + * "sha256:1191b3f5862aa9231858809b7ac8b91c0b727ce85c9b3279932f0baacc92967d", + * "sha256:9978d084fd771e0b3d1acd7f3525d1b25288ababe9ad8ed259b36101e4e3addd", + * "sha256:2f4f74d3821ecbdd60b5d932452ea9e30cecf902334165c4a19837f6ee636377", + * "sha256:003bb6178bc3218242d73e51d5e9ab2f991dc607780194719c6bd4c8c412fe8c", + * "sha256:15b32d849da2239b1af583f9381c7a75d7aceba12f5ddfffa7a059116cf05ab9", + * "sha256:6e5c5f6bf043bc634378b1e4b61af09be74741f2ac80204d7a373713b1fd5a40", + * "sha256:3260e00e353bfb765b25597d13868c2ef64cb3d509875abcfb58c4e9bf7f4ee2", + * "sha256:f3274b75856311e92e14a1270c78737c86456d6353fe4a83bd2e81bcd2a996ea" + * ] + * } + * } + * ] + */ \ No newline at end of file diff --git a/packages/aws-cdk/lib/logging.ts b/packages/aws-cdk/lib/logging.ts index 03405da3fdb1b..d9b6ef87a2900 100644 --- a/packages/aws-cdk/lib/logging.ts +++ b/packages/aws-cdk/lib/logging.ts @@ -3,7 +3,7 @@ import util = require('util'); // tslint:disable:no-console the whole point of those methods is precisely to output to the console... -let isVerbose = false; +export let isVerbose = false; export function setVerbose(enabled = true) { isVerbose = enabled; diff --git a/packages/aws-cdk/lib/os.ts b/packages/aws-cdk/lib/os.ts new file mode 100644 index 0000000000000..f12168bba8f8f --- /dev/null +++ b/packages/aws-cdk/lib/os.ts @@ -0,0 +1,98 @@ +import child_process = require("child_process"); +import colors = require('colors/safe'); +import { debug } from "./logging"; + +export interface ShellOptions extends child_process.SpawnOptions { + quiet?: boolean; +} + +/** + * OS helpers + * + * Shell function which both prints to stdout and collects the output into a + * string. + */ +export async function shell(command: string[], options: ShellOptions = {}): Promise { + debug(`Executing ${colors.blue(renderCommandLine(command))}`); + const child = child_process.spawn(command[0], command.slice(1), { + ...options, + stdio: [ 'ignore', 'pipe', 'inherit' ] + }); + + return new Promise((resolve, reject) => { + const stdout = new Array(); + + // Both write to stdout and collect + child.stdout.on('data', chunk => { + if (!options.quiet) { + process.stdout.write(chunk); + } + stdout.push(chunk); + }); + + child.once('error', reject); + + child.once('exit', code => { + if (code === 0) { + resolve(Buffer.concat(stdout).toString('utf-8')); + } else { + reject(new Error(`${renderCommandLine(command)} exited with error code ${code}`)); + } + }); + }); +} + +/** + * Render the given command line as a string + * + * Probably missing some cases but giving it a good effort. + */ +function renderCommandLine(cmd: string[]) { + if (process.platform !== 'win32') { + return doRender(cmd, hasAnyChars(' ', '\\', '!', '"', "'", '&', '$'), posixEscape); + } else { + return doRender(cmd, hasAnyChars(' ', '"', '&', '^', '%'), windowsEscape); + } +} + +/** + * Render a UNIX command line + */ +function doRender(cmd: string[], needsEscaping: (x: string) => boolean, doEscape: (x: string) => string): string { + return cmd.map(x => needsEscaping(x) ? doEscape(x) : x).join(' '); +} + +/** + * Return a predicate that checks if a string has any of the indicated chars in it + */ +function hasAnyChars(...chars: string[]): (x: string) => boolean { + return (str: string) => { + return chars.some(c => str.indexOf(c) !== -1); + }; +} + +/** + * Escape a shell argument for POSIX shells + * + * Wrapping in single quotes and escaping single quotes inside will do it for us. + */ +function posixEscape(x: string) { + // Turn ' -> '"'"' + x = x.replace("'", "'\"'\"'"); + return `'${x}'`; +} + +/** + * Escape a shell argument for cmd.exe + * + * This is how to do it right, but I'm not following everything: + * + * https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ + */ +function windowsEscape(x: string): string { + // First surround by double quotes, ignore the part about backslashes + x = `"${x}"`; + // Now escape all special characters + const shellMeta = ['"', '&', '^', '%']; + return x.split('').map(c => shellMeta.indexOf(x) !== -1 ? '^' + c : c).join(''); +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/util/please-hold.ts b/packages/aws-cdk/lib/util/please-hold.ts new file mode 100644 index 0000000000000..044ff00259a6a --- /dev/null +++ b/packages/aws-cdk/lib/util/please-hold.ts @@ -0,0 +1,25 @@ +import { warning } from "../logging"; + +/** + * Print a message to the logger in case the operation takes a long time + */ +export class PleaseHold { + private handle?: NodeJS.Timer; + + constructor(private readonly message: string, private readonly timeoutSec = 10) { + } + + public start() { + this.handle = setTimeout(this.printMessage.bind(this), this.timeoutSec * 1000); + } + + public stop() { + if (this.handle) { + clearTimeout(this.handle); + } + } + + private printMessage() { + warning(this.message); + } +} \ No newline at end of file From 4baaeef6813568615be5a5fe269931d80eb28154 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 18 Oct 2018 16:10:54 +0200 Subject: [PATCH 086/140] feat(aws-cdk): allow uploading Docker images as assets The construct library that takes advantage of this feature is in a separate branch, but this change already adds the ability for the toolkit to upload images. The code is integration tested in the other branch as well. --- packages/@aws-cdk/assets/lib/asset.ts | 2 +- packages/@aws-cdk/cx-api/lib/cxapi.ts | 42 ++++- packages/aws-cdk/lib/api/toolkit-info.ts | 96 ++++++++++- packages/aws-cdk/lib/api/util/sdk.ts | 7 + packages/aws-cdk/lib/assets.ts | 13 +- packages/aws-cdk/lib/docker.ts | 208 +++++++++++++++++++++++ packages/aws-cdk/lib/logging.ts | 2 +- packages/aws-cdk/lib/os.ts | 98 +++++++++++ packages/aws-cdk/lib/util/please-hold.ts | 25 +++ 9 files changed, 480 insertions(+), 13 deletions(-) create mode 100644 packages/aws-cdk/lib/docker.ts create mode 100644 packages/aws-cdk/lib/os.ts create mode 100644 packages/aws-cdk/lib/util/please-hold.ts diff --git a/packages/@aws-cdk/assets/lib/asset.ts b/packages/@aws-cdk/assets/lib/asset.ts index ffef5ebcb3cef..ba26f3a737269 100644 --- a/packages/@aws-cdk/assets/lib/asset.ts +++ b/packages/@aws-cdk/assets/lib/asset.ts @@ -124,7 +124,7 @@ export class Asset extends cdk.Construct { // for tooling to be able to package and upload a directory to the // s3 bucket and plug in the bucket name and key in the correct // parameters. - const asset: cxapi.AssetMetadataEntry = { + const asset: cxapi.FileAssetMetadataEntry = { path: this.assetPath, id: this.uniqueId, packaging: props.packaging, diff --git a/packages/@aws-cdk/cx-api/lib/cxapi.ts b/packages/@aws-cdk/cx-api/lib/cxapi.ts index 56a96d12855be..1ab3431299f9b 100644 --- a/packages/@aws-cdk/cx-api/lib/cxapi.ts +++ b/packages/@aws-cdk/cx-api/lib/cxapi.ts @@ -79,7 +79,13 @@ export const DEFAULT_ACCOUNT_CONTEXT_KEY = 'aws:cdk:toolkit:default-account'; export const DEFAULT_REGION_CONTEXT_KEY = 'aws:cdk:toolkit:default-region'; export const ASSET_METADATA = 'aws:cdk:asset'; -export interface AssetMetadataEntry { + +export interface FileAssetMetadataEntry { + /** + * Requested packaging style + */ + packaging: 'zip' | 'file'; + /** * Path on disk to the asset */ @@ -90,11 +96,6 @@ export interface AssetMetadataEntry { */ id: string; - /** - * Requested packaging style - */ - packaging: 'zip' | 'file'; - /** * Name of parameter where S3 bucket should be passed in */ @@ -106,6 +107,35 @@ export interface AssetMetadataEntry { s3KeyParameter: string; } +export interface ContainerImageAssetMetadataEntry { + /** + * Type of asset + */ + packaging: 'container-image'; + + /** + * Path on disk to the asset + */ + path: string; + + /** + * Logical identifier for the asset + */ + id: string; + + /** + * Name of the parameter that takes the repository name + */ + repositoryParameter: string; + + /** + * Name of the parameter that takes the tag + */ + tagParameter: string; +} + +export type AssetMetadataEntry = FileAssetMetadataEntry | ContainerImageAssetMetadataEntry; + /** * Metadata key used to print INFO-level messages by the toolkit when an app is syntheized. */ diff --git a/packages/aws-cdk/lib/api/toolkit-info.ts b/packages/aws-cdk/lib/api/toolkit-info.ts index 55465bd3747d6..5b19e66d20da0 100644 --- a/packages/aws-cdk/lib/api/toolkit-info.ts +++ b/packages/aws-cdk/lib/api/toolkit-info.ts @@ -21,12 +21,15 @@ export interface Uploaded { } export class ToolkitInfo { + public readonly sdk: SDK; + constructor(private readonly props: { sdk: SDK, bucketName: string, bucketEndpoint: string, environment: cxapi.Environment - }) { } + }) { + } public get bucketUrl() { return `https://${this.props.bucketEndpoint}`; @@ -73,6 +76,82 @@ export class ToolkitInfo { return { filename, key, changed: true }; } + /** + * Prepare an ECR repository for uploading to using Docker + */ + public async prepareEcrRepository(id: string, imageTag: string): Promise { + const ecr = await this.props.sdk.ecr(this.props.environment, Mode.ForWriting); + + // Create the repository if it doesn't exist yet + const repositoryName = 'cdk/' + id.replace(/[:/]/g, '-').toLowerCase(); + + let repository; + try { + const describeResponse = await ecr.describeRepositories({ repositoryNames: [repositoryName] }).promise(); + repository = describeResponse.repositories![0]; + } catch (e) { + if (e.code !== 'RepositoryNotFoundException') { throw e; } + } + + if (repository) { + try { + await ecr.describeImages({ repositoryName, imageIds: [{ imageTag }] }).promise(); + + // If we got here, the image already exists. Nothing else needs to be done. + return { + alreadyExists: true, + repositoryUri: repository.repositoryUri!, + repositoryArn: repository.repositoryArn!, + }; + } catch (e) { + if (e.code !== 'ImageNotFoundException') { throw e; } + } + } else { + const response = await ecr.createRepository({ repositoryName }).promise(); + repository = response.repository!; + + // Better put a lifecycle policy on this so as to not cost too much money + await ecr.putLifecyclePolicy({ + repositoryName, + lifecyclePolicyText: JSON.stringify(DEFAULT_REPO_LIFECYCLE) + }).promise(); + } + + // The repo exists, image just needs to be uploaded. Get auth to do so. + + const authData = (await ecr.getAuthorizationToken({ }).promise()).authorizationData || []; + if (authData.length === 0) { + throw new Error('No authorization data received from ECR'); + } + const token = Buffer.from(authData[0].authorizationToken!, 'base64').toString('ascii'); + const [username, password] = token.split(':'); + + return { + alreadyExists: false, + repositoryUri: repository.repositoryUri!, + repositoryArn: repository.repositoryArn!, + username, + password, + endpoint: authData[0].proxyEndpoint!, + }; + } +} + +export type EcrRepositoryInfo = CompleteEcrRepositoryInfo | UploadableEcrRepositoryInfo; + +export interface CompleteEcrRepositoryInfo { + repositoryUri: string; + repositoryArn: string; + alreadyExists: true; +} + +export interface UploadableEcrRepositoryInfo { + repositoryUri: string; + repositoryArn: string; + alreadyExists: false; + username: string; + password: string; + endpoint: string; } async function objectExists(s3: aws.S3, bucket: string, key: string) { @@ -114,3 +193,18 @@ function getOutputValue(stack: aws.CloudFormation.Stack, output: string): string } return result; } + +const DEFAULT_REPO_LIFECYCLE = { + rules: [ + { + rulePriority: 100, + description: 'Retain only 5 images', + selection: { + tagStatus: 'any', + countType: 'imageCountMoreThan', + countNumber: 5, + }, + action: { type: 'expire' } + } + ] +}; \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/util/sdk.ts b/packages/aws-cdk/lib/api/util/sdk.ts index 9b795e0ef1858..9c8ca43ac7177 100644 --- a/packages/aws-cdk/lib/api/util/sdk.ts +++ b/packages/aws-cdk/lib/api/util/sdk.ts @@ -104,6 +104,13 @@ export class SDK { }); } + public async ecr(environment: Environment, mode: Mode): Promise { + return new AWS.ECR({ + region: environment.region, + credentials: await this.credentialsCache.get(environment.account, mode) + }); + } + public async defaultRegion(): Promise { return await getCLICompatibleDefaultRegion(this.profile); } diff --git a/packages/aws-cdk/lib/assets.ts b/packages/aws-cdk/lib/assets.ts index a264b2a3eeee8..7a30f8500b5fe 100644 --- a/packages/aws-cdk/lib/assets.ts +++ b/packages/aws-cdk/lib/assets.ts @@ -1,10 +1,12 @@ -import { ASSET_METADATA, ASSET_PREFIX_SEPARATOR, AssetMetadataEntry, StackMetadata, SynthesizedStack } from '@aws-cdk/cx-api'; +// tslint:disable-next-line:max-line-length +import { ASSET_METADATA, ASSET_PREFIX_SEPARATOR, AssetMetadataEntry, FileAssetMetadataEntry, StackMetadata, SynthesizedStack } from '@aws-cdk/cx-api'; import { CloudFormation } from 'aws-sdk'; import fs = require('fs-extra'); import os = require('os'); import path = require('path'); import { ToolkitInfo } from './api/toolkit-info'; import { zipDirectory } from './archive'; +import { prepareContainerAsset } from './docker'; import { debug, success } from './logging'; export async function prepareAssets(stack: SynthesizedStack, toolkitInfo?: ToolkitInfo): Promise { @@ -35,12 +37,15 @@ async function prepareAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitInfo) return await prepareZipAsset(asset, toolkitInfo); case 'file': return await prepareFileAsset(asset, toolkitInfo); + case 'container-image': + return await prepareContainerAsset(asset, toolkitInfo); default: - throw new Error(`Unsupported packaging type: ${asset.packaging}`); + // tslint:disable-next-line:max-line-length + throw new Error(`Unsupported packaging type: ${(asset as any).packaging}. You might need to upgrade your aws-cdk toolkit to support this asset type.`); } } -async function prepareZipAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitInfo): Promise { +async function prepareZipAsset(asset: FileAssetMetadataEntry, toolkitInfo: ToolkitInfo): Promise { debug('Preparing zip asset from directory:', asset.path); const staging = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-assets')); try { @@ -60,7 +65,7 @@ async function prepareZipAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitIn * @param contentType Content-type to use when uploading to S3 (none will be specified by default) */ async function prepareFileAsset( - asset: AssetMetadataEntry, + asset: FileAssetMetadataEntry, toolkitInfo: ToolkitInfo, filePath?: string, contentType?: string): Promise { diff --git a/packages/aws-cdk/lib/docker.ts b/packages/aws-cdk/lib/docker.ts new file mode 100644 index 0000000000000..0fd0c3c54f6fa --- /dev/null +++ b/packages/aws-cdk/lib/docker.ts @@ -0,0 +1,208 @@ +import { ContainerImageAssetMetadataEntry } from '@aws-cdk/cx-api'; +import { CloudFormation } from 'aws-sdk'; +import crypto = require('crypto'); +import { ToolkitInfo } from './api/toolkit-info'; +import { debug, print } from './logging'; +import { shell } from './os'; +import { PleaseHold } from './util/please-hold'; + +/** + * Build and upload a Docker image + * + * Permanently identifying images is a bit of a bust. Newer Docker version use + * a digest (sha256:xxxx) as an image identifier, which is pretty good to avoid + * spurious rebuilds. However, this digest is calculated over a manifest that + * includes metadata that is liable to change. For example, as soon as we + * push the Docker image to a repository, the digest changes. This makes the + * digest worthless to determe whether we already pushed an image, for example. + * + * As a workaround, we calculate our own digest over parts of the manifest that + * are unlikely to change, and tag based on that. + */ +export async function prepareContainerAsset(asset: ContainerImageAssetMetadataEntry, toolkitInfo: ToolkitInfo): Promise { + debug(' 👑 Preparing Docker image asset:', asset.path); + + const buildHold = new PleaseHold(` ⌛ Building Docker image for ${asset.path}; this may take a while.`); + try { + buildHold.start(); + + const command = ['docker', + 'build', + '--quiet', + asset.path]; + const imageId = (await shell(command, { quiet: true })).trim(); + buildHold.stop(); + + const tag = await calculateImageDigest(imageId); + + debug(` ⌛ Image has tag ${tag}, preparing ECR repository`); + const ecr = await toolkitInfo.prepareEcrRepository(asset.id, tag); + + if (ecr.alreadyExists) { + debug(' 👑 Image already uploaded.'); + } else { + // Login and push + debug(` ⌛ Image needs to be uploaded first.`); + + await shell(['docker', 'login', + '--username', ecr.username, + '--password', ecr.password, + ecr.endpoint]); + + const qualifiedImageName = `${ecr.repositoryUri}:${tag}`; + await shell(['docker', 'tag', imageId, qualifiedImageName]); + + // There's no way to make this quiet, so we can't use a PleaseHold. Print a header message. + print(` ⌛ Pusing Docker image for ${asset.path}; this may take a while.`); + await shell(['docker', 'push', qualifiedImageName]); + } + + return [ + { ParameterKey: asset.repositoryParameter, ParameterValue: ecr.repositoryArn }, + { ParameterKey: asset.tagParameter, ParameterValue: tag }, + ]; + } catch (e) { + if (e.code === 'ENOENT') { + // tslint:disable-next-line:max-line-length + throw new Error('Error building Docker image asset; you need to have Docker installed in order to be able to build image assets. Please install Docker and try again.'); + } + throw e; + } finally { + buildHold.stop(); + } +} + +/** + * Calculate image digest + */ +async function calculateImageDigest(imageId: string) { + const manifest = await shell(['docker', 'inspect', imageId], { quiet: true }); + const parsed = JSON.parse(manifest)[0]; + + const importantParts = { + Parent: parsed.Parent, + Comment: parsed.Comment, + Config: parsed.Config, + RootFS: parsed.RootFS, + Architecture: parsed.Architecture, + Os: parsed.Os, + Size: parsed.Size, + VirtualSize: parsed.VirtualSize, + GraphDriver: parsed.GraphDriver + }; + + return crypto.createHash('sha256').update(JSON.stringify(importantParts)).digest('hex'); +} + +/** + * Example of a Docker manifest + * + * [ + * { + * "Id": "sha256:3a90542991d03007fd1d8f3b3a6ab04ebb02386785430fe48a867768a048d828", + * "RepoTags": [ + * "993655754359.dkr.ecr.us-east-1.amazonaws.com/cdk/awsecsintegimage7c15b8c6:latest" + * ], + * "RepoDigests": [ + * "993655754359.dkr.ecr.us-east-1.amazo....5e50c0cfc3f2355191934b05df68cd3339a044959111ffec2e14765" + * ], + * "Parent": "sha256:465720f8f43c9c0aff5dcc731d4e368a3927cae4e885442d4ba0bf8a867b7561", + * "Comment": "", + * "Created": "2018-10-17T10:16:40.775888476Z", + * "Container": "20f145d2e7fbf126ca9f4422497b932bc96b5faa038dc032de1e246f64e03a66", + * "ContainerConfig": { + * "Hostname": "9b48b580a312", + * "Domainname": "", + * "User": "", + * "AttachStdin": false, + * "AttachStdout": false, + * "AttachStderr": false, + * "ExposedPorts": { + * "8000/tcp": {} + * }, + * "Tty": false, + * "OpenStdin": false, + * "StdinOnce": false, + * "Env": [ + * "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + * "LANG=C.UTF-8", + * "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D", + * "PYTHON_VERSION=3.6.6", + * "PYTHON_PIP_VERSION=18.1" + * ], + * "Cmd": [ + * "/bin/sh", + * "-c", + * "#(nop) ", + * "CMD [\"/bin/sh\" \"-c\" \"python3 index.py\"]" + * ], + * "ArgsEscaped": true, + * "Image": "sha256:465720f8f43c9c0aff5dcc731d4e368a3927cae4e885442d4ba0bf8a867b7561", + * "Volumes": null, + * "WorkingDir": "/code", + * "Entrypoint": null, + * "OnBuild": [], + * "Labels": {} + * }, + * "DockerVersion": "17.03.2-ce", + * "Author": "", + * "Config": { + * "Hostname": "9b48b580a312", + * "Domainname": "", + * "User": "", + * "AttachStdin": false, + * "AttachStdout": false, + * "AttachStderr": false, + * "ExposedPorts": { + * "8000/tcp": {} + * }, + * "Tty": false, + * "OpenStdin": false, + * "StdinOnce": false, + * "Env": [ + * "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + * "LANG=C.UTF-8", + * "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D", + * "PYTHON_VERSION=3.6.6", + * "PYTHON_PIP_VERSION=18.1" + * ], + * "Cmd": [ + * "/bin/sh", + * "-c", + * "python3 index.py" + * ], + * "ArgsEscaped": true, + * "Image": "sha256:465720f8f43c9c0aff5dcc731d4e368a3927cae4e885442d4ba0bf8a867b7561", + * "Volumes": null, + * "WorkingDir": "/code", + * "Entrypoint": null, + * "OnBuild": [], + * "Labels": {} + * }, + * "Architecture": "amd64", + * "Os": "linux", + * "Size": 917730468, + * "VirtualSize": 917730468, + * "GraphDriver": { + * "Name": "aufs", + * "Data": null + * }, + * "RootFS": { + * "Type": "layers", + * "Layers": [ + * "sha256:f715ed19c28b66943ac8bc12dbfb828e8394de2530bbaf1ecce906e748e4fdff", + * "sha256:8bb25f9cdc41e7d085033af15a522973b44086d6eedd24c11cc61c9232324f77", + * "sha256:08a01612ffca33483a1847c909836610610ce523fb7e1aca880140ee84df23e9", + * "sha256:1191b3f5862aa9231858809b7ac8b91c0b727ce85c9b3279932f0baacc92967d", + * "sha256:9978d084fd771e0b3d1acd7f3525d1b25288ababe9ad8ed259b36101e4e3addd", + * "sha256:2f4f74d3821ecbdd60b5d932452ea9e30cecf902334165c4a19837f6ee636377", + * "sha256:003bb6178bc3218242d73e51d5e9ab2f991dc607780194719c6bd4c8c412fe8c", + * "sha256:15b32d849da2239b1af583f9381c7a75d7aceba12f5ddfffa7a059116cf05ab9", + * "sha256:6e5c5f6bf043bc634378b1e4b61af09be74741f2ac80204d7a373713b1fd5a40", + * "sha256:3260e00e353bfb765b25597d13868c2ef64cb3d509875abcfb58c4e9bf7f4ee2", + * "sha256:f3274b75856311e92e14a1270c78737c86456d6353fe4a83bd2e81bcd2a996ea" + * ] + * } + * } + * ] + */ \ No newline at end of file diff --git a/packages/aws-cdk/lib/logging.ts b/packages/aws-cdk/lib/logging.ts index 03405da3fdb1b..d9b6ef87a2900 100644 --- a/packages/aws-cdk/lib/logging.ts +++ b/packages/aws-cdk/lib/logging.ts @@ -3,7 +3,7 @@ import util = require('util'); // tslint:disable:no-console the whole point of those methods is precisely to output to the console... -let isVerbose = false; +export let isVerbose = false; export function setVerbose(enabled = true) { isVerbose = enabled; diff --git a/packages/aws-cdk/lib/os.ts b/packages/aws-cdk/lib/os.ts new file mode 100644 index 0000000000000..f12168bba8f8f --- /dev/null +++ b/packages/aws-cdk/lib/os.ts @@ -0,0 +1,98 @@ +import child_process = require("child_process"); +import colors = require('colors/safe'); +import { debug } from "./logging"; + +export interface ShellOptions extends child_process.SpawnOptions { + quiet?: boolean; +} + +/** + * OS helpers + * + * Shell function which both prints to stdout and collects the output into a + * string. + */ +export async function shell(command: string[], options: ShellOptions = {}): Promise { + debug(`Executing ${colors.blue(renderCommandLine(command))}`); + const child = child_process.spawn(command[0], command.slice(1), { + ...options, + stdio: [ 'ignore', 'pipe', 'inherit' ] + }); + + return new Promise((resolve, reject) => { + const stdout = new Array(); + + // Both write to stdout and collect + child.stdout.on('data', chunk => { + if (!options.quiet) { + process.stdout.write(chunk); + } + stdout.push(chunk); + }); + + child.once('error', reject); + + child.once('exit', code => { + if (code === 0) { + resolve(Buffer.concat(stdout).toString('utf-8')); + } else { + reject(new Error(`${renderCommandLine(command)} exited with error code ${code}`)); + } + }); + }); +} + +/** + * Render the given command line as a string + * + * Probably missing some cases but giving it a good effort. + */ +function renderCommandLine(cmd: string[]) { + if (process.platform !== 'win32') { + return doRender(cmd, hasAnyChars(' ', '\\', '!', '"', "'", '&', '$'), posixEscape); + } else { + return doRender(cmd, hasAnyChars(' ', '"', '&', '^', '%'), windowsEscape); + } +} + +/** + * Render a UNIX command line + */ +function doRender(cmd: string[], needsEscaping: (x: string) => boolean, doEscape: (x: string) => string): string { + return cmd.map(x => needsEscaping(x) ? doEscape(x) : x).join(' '); +} + +/** + * Return a predicate that checks if a string has any of the indicated chars in it + */ +function hasAnyChars(...chars: string[]): (x: string) => boolean { + return (str: string) => { + return chars.some(c => str.indexOf(c) !== -1); + }; +} + +/** + * Escape a shell argument for POSIX shells + * + * Wrapping in single quotes and escaping single quotes inside will do it for us. + */ +function posixEscape(x: string) { + // Turn ' -> '"'"' + x = x.replace("'", "'\"'\"'"); + return `'${x}'`; +} + +/** + * Escape a shell argument for cmd.exe + * + * This is how to do it right, but I'm not following everything: + * + * https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ + */ +function windowsEscape(x: string): string { + // First surround by double quotes, ignore the part about backslashes + x = `"${x}"`; + // Now escape all special characters + const shellMeta = ['"', '&', '^', '%']; + return x.split('').map(c => shellMeta.indexOf(x) !== -1 ? '^' + c : c).join(''); +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/util/please-hold.ts b/packages/aws-cdk/lib/util/please-hold.ts new file mode 100644 index 0000000000000..044ff00259a6a --- /dev/null +++ b/packages/aws-cdk/lib/util/please-hold.ts @@ -0,0 +1,25 @@ +import { warning } from "../logging"; + +/** + * Print a message to the logger in case the operation takes a long time + */ +export class PleaseHold { + private handle?: NodeJS.Timer; + + constructor(private readonly message: string, private readonly timeoutSec = 10) { + } + + public start() { + this.handle = setTimeout(this.printMessage.bind(this), this.timeoutSec * 1000); + } + + public stop() { + if (this.handle) { + clearTimeout(this.handle); + } + } + + private printMessage() { + warning(this.message); + } +} \ No newline at end of file From 7b19dd2bb3107e64aa4e8d5c5ffa24f640f13f95 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 22 Oct 2018 13:48:09 -0700 Subject: [PATCH 087/140] Add LoadBalancedEcsService L3 Construct --- .../hello-cdk-ecs/index.ts | 162 +++++++++--------- packages/@aws-cdk/aws-ecs/lib/index.ts | 1 + .../aws-ecs/lib/load-balanced-ecs-service.ts | 95 ++++++++++ 3 files changed, 180 insertions(+), 78 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index c392b053ba36a..8d022e90561b5 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -10,88 +10,18 @@ class BonjourECS extends cdk.Stack { // a separate stack and import it here. We then have two stacks to // deploy, but VPC creation is slow so we'll only have to do that once // and can iterate quickly on consuming stacks. Not doing that for now. - const vpc = new ec2.VpcNetwork(this, 'MyVpc', { - maxAZs: 2 - }); - - const cluster = new ecs.EcsCluster(this, 'EcsCluster', { - vpc - }); + const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); + const cluster = new ecs.EcsCluster(this, 'EcsCluster', { vpc }); - // name, image, cpu, memory, port (with default) - // - // Include in constructs: - // - networking - include SD, ALB - // - logging - cloudwatch logs integration? talk to nathan about 3rd - // party integrations - aggregated logging across the service - // (instead of per task). Probably prometheus or elk? - // - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, - // zipkin. - // - so x-ray is a container that is hooked up to sidecars that come - // with the application container itself - // - autoscaling - application autoscaling (Fargate focused?) - - const taskDefinition = new ecs.EcsTaskDefinition(this, "EcsTD", { - family: "ecs-task-definition", - }); - - const container = taskDefinition.addContainer('web', { - image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), - cpu: 1024, + // Instantiate ECS Service with just cluster and image + const ecsService = new ecs.LoadBalancedEcsService(this, "EcsService", { + cluster, memoryLimitMiB: 512, - essential: true - }); - - container.linuxParameters.addCapabilities(ecs.Capability.All); - container.linuxParameters.dropCapabilities(ecs.Capability.Chown); - - container.linuxParameters.addDevices({ - containerPath: "/dev/pudding", - hostPath: "/dev/clyde", - permissions: [ecs.DevicePermission.Read] - }); - - container.linuxParameters.addTmpfs({ - containerPath: "/dev/sda", - size: 12345, - mountOptions: [ecs.TmpfsMountOption.Ro] - }); - - container.linuxParameters.sharedMemorySize = 65535; - container.linuxParameters.initProcessEnabled = true; - - container.addUlimits({ - name: ecs.UlimitName.Core, - softLimit: 1234, - hardLimit: 1234, - }); - - container.addPortMappings({ - containerPort: 80, - // hostPort: 80, - protocol: ecs.Protocol.Tcp, - }); - - container.addMountPoints({ - containerPath: '/tmp/cache', - sourceVolume: 'volume-1', - readOnly: true, - }, { - containerPath: './cache', - sourceVolume: 'volume-2', - readOnly: true, - }); - - container.addVolumesFrom({ - sourceContainer: 'web', - readOnly: true, + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), }); - new ecs.EcsService(this, "EcsService", { - cluster, - taskDefinition, - desiredCount: 1, - }); + // Output the DNS where you can access your service + new cdk.Output(this, 'LoadBalancerDNS', { value: ecsService.loadBalancer.dnsName }); } } @@ -100,3 +30,79 @@ const app = new cdk.App(); new BonjourECS(app, 'Bonjour'); app.run(); + +// name, image, cpu, memory, port (with default) +// +// Include in constructs: +// - networking - include SD, ALB +// - logging - cloudwatch logs integration? talk to nathan about 3rd +// party integrations - aggregated logging across the service +// (instead of per task). Probably prometheus or elk? +// - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, +// zipkin. +// - so x-ray is a container that is hooked up to sidecars that come +// with the application container itself +// - autoscaling - application autoscaling (Fargate focused?) + +// const taskDefinition = new ecs.EcsTaskDefinition(this, "EcsTD", { +// family: "ecs-task-definition", +// }); + +// const container = taskDefinition.addContainer('web', { +// image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), +// cpu: 1024, +// memoryLimitMiB: 512, +// essential: true +// }); + +// container.linuxParameters.addCapabilities(ecs.Capability.All); +// container.linuxParameters.dropCapabilities(ecs.Capability.Chown); + +// container.linuxParameters.addDevices({ +// containerPath: "/dev/pudding", +// hostPath: "/dev/clyde", +// permissions: [ecs.DevicePermission.Read] +// }); + +// container.linuxParameters.addTmpfs({ +// containerPath: "/dev/sda", +// size: 12345, +// mountOptions: [ecs.TmpfsMountOption.Ro] +// }); + +// container.linuxParameters.sharedMemorySize = 65535; +// container.linuxParameters.initProcessEnabled = true; + +// container.addUlimits({ +// name: ecs.UlimitName.Core, +// softLimit: 1234, +// hardLimit: 1234, +// }); + +// container.addPortMappings({ +// containerPort: 80, +// // hostPort: 80, +// protocol: ecs.Protocol.Tcp, +// }); + +// container.addMountPoints({ +// containerPath: '/tmp/cache', +// sourceVolume: 'volume-1', +// readOnly: true, +// }, { +// containerPath: './cache', +// sourceVolume: 'volume-2', +// readOnly: true, +// }); + +// container.addVolumesFrom({ +// sourceContainer: 'web', +// readOnly: true, +// }); + +// new ecs.EcsService(this, "EcsService", { +// cluster, +// taskDefinition, +// desiredCount: 1, +// }); +// } diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 0c06c5155f709..b163abbafa24b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -12,6 +12,7 @@ export * from './fargate/fargate-task-definition'; export * from './container-definition'; export * from './container-image'; export * from './linux-parameters'; +export * from './load-balanced-ecs-service'; export * from './load-balanced-fargate-service'; export * from './load-balanced-fargate-service-applet'; diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts new file mode 100644 index 0000000000000..839263d6e927e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts @@ -0,0 +1,95 @@ +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import { ContainerImage } from './container-image'; +import { IEcsCluster } from './ecs/ecs-cluster'; +import { EcsService } from './ecs/ecs-service'; +import { EcsTaskDefinition } from './ecs/ecs-task-definition'; + +export interface LoadBalancedEcsServiceProps { + /** + * The cluster where your Fargate service will be deployed + */ + cluster: IEcsCluster; + + /** + * The image to use for a container. + * + * You can use images in the Docker Hub registry or specify other + * repositories (repository-url/image:tag). + */ + image: ContainerImage; + + /** + * The hard limit (in MiB) of memory to present to the container. + * + * If your container attempts to exceed the allocated memory, the container + * is terminated. + * + * At least one of memoryLimitMiB and memoryReservationMiB is required. + */ + memoryLimitMiB?: number; + + /** + * The soft limit (in MiB) of memory to reserve for the container. + * + * When system memory is under contention, Docker attempts to keep the + * container memory within the limit. If the container requires more memory, + * it can consume up to the value specified by the Memory property or all of + * the available memory on the container instance—whichever comes first. + * + * At least one of memoryLimitMiB and memoryReservationMiB is required. + */ + memoryReservationMiB?: number; + + /** + * The container port of the application load balancer attached to your Fargate service. Corresponds to container port mapping. + * + * @default 80 + */ + containerPort?: number; + + /** + * Determines whether the Application Load Balancer will be internet-facing + * + * @default true + */ + publicLoadBalancer?: boolean; +} + +export class LoadBalancedEcsService extends cdk.Construct { + public readonly loadBalancer: elbv2.ApplicationLoadBalancer; + + constructor(parent: cdk.Construct, id: string, props: LoadBalancedEcsServiceProps) { + super(parent, id); + + const taskDefinition = new EcsTaskDefinition(this, 'TaskDef', {}); + + const container = taskDefinition.addContainer('web', { + image: props.image, + memoryLimitMiB: props.memoryLimitMiB, + }); + + container.addPortMappings({ + containerPort: props.containerPort || 80, + }); + + const service = new EcsService(this, "Service", { + cluster: props.cluster, + taskDefinition, + }); + + const internetFacing = props.publicLoadBalancer !== undefined ? props.publicLoadBalancer : true; + const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { + vpc: props.cluster.vpc, + internetFacing + }); + + this.loadBalancer = lb; + + const listener = lb.addListener('PublicListener', { port: 80, open: true }); + listener.addTargets('ECS', { + port: 80, + targets: [service] + }); + } +} From 71766e169176c17c8da557e2adf4a85ea73da0d6 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 25 Oct 2018 12:13:52 +0200 Subject: [PATCH 088/140] More logging, blacklist manifest parts. --- packages/aws-cdk/lib/api/toolkit-info.ts | 4 ++ packages/aws-cdk/lib/docker.ts | 50 +++++++++++++++--------- packages/aws-cdk/lib/logging.ts | 2 +- packages/aws-cdk/lib/os.ts | 4 +- packages/aws-cdk/lib/util/please-hold.ts | 5 ++- 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/packages/aws-cdk/lib/api/toolkit-info.ts b/packages/aws-cdk/lib/api/toolkit-info.ts index 5b19e66d20da0..99e0692d3e1f8 100644 --- a/packages/aws-cdk/lib/api/toolkit-info.ts +++ b/packages/aws-cdk/lib/api/toolkit-info.ts @@ -87,6 +87,7 @@ export class ToolkitInfo { let repository; try { + debug(`${repositoryName}: checking for repository.`); const describeResponse = await ecr.describeRepositories({ repositoryNames: [repositoryName] }).promise(); repository = describeResponse.repositories![0]; } catch (e) { @@ -95,6 +96,7 @@ export class ToolkitInfo { if (repository) { try { + debug(`${repositoryName}: checking for image ${imageTag}`); await ecr.describeImages({ repositoryName, imageIds: [{ imageTag }] }).promise(); // If we got here, the image already exists. Nothing else needs to be done. @@ -107,6 +109,7 @@ export class ToolkitInfo { if (e.code !== 'ImageNotFoundException') { throw e; } } } else { + debug(`${repositoryName}: creating`); const response = await ecr.createRepository({ repositoryName }).promise(); repository = response.repository!; @@ -119,6 +122,7 @@ export class ToolkitInfo { // The repo exists, image just needs to be uploaded. Get auth to do so. + debug(`Fetching ECR authorization token`); const authData = (await ecr.getAuthorizationToken({ }).promise()).authorizationData || []; if (authData.length === 0) { throw new Error('No authorization data received from ECR'); diff --git a/packages/aws-cdk/lib/docker.ts b/packages/aws-cdk/lib/docker.ts index 0fd0c3c54f6fa..14f7bc2df5ec7 100644 --- a/packages/aws-cdk/lib/docker.ts +++ b/packages/aws-cdk/lib/docker.ts @@ -33,7 +33,7 @@ export async function prepareContainerAsset(asset: ContainerImageAssetMetadataEn const imageId = (await shell(command, { quiet: true })).trim(); buildHold.stop(); - const tag = await calculateImageDigest(imageId); + const tag = await calculateImageFingerprint(imageId); debug(` ⌛ Image has tag ${tag}, preparing ECR repository`); const ecr = await toolkitInfo.prepareEcrRepository(asset.id, tag); @@ -55,6 +55,7 @@ export async function prepareContainerAsset(asset: ContainerImageAssetMetadataEn // There's no way to make this quiet, so we can't use a PleaseHold. Print a header message. print(` ⌛ Pusing Docker image for ${asset.path}; this may take a while.`); await shell(['docker', 'push', qualifiedImageName]); + debug(` 👑 Docker image for ${asset.path} pushed.`); } return [ @@ -73,25 +74,36 @@ export async function prepareContainerAsset(asset: ContainerImageAssetMetadataEn } /** - * Calculate image digest + * Calculate image fingerprint. + * + * The fingerprint has a high likelihood to be the same across repositories. + * (As opposed to Docker's built-in image digest, which changes as soon + * as the image is uploaded since it includes the tags that an image has). + * + * The fingerprint will be used as a tag to identify a particular image. */ -async function calculateImageDigest(imageId: string) { - const manifest = await shell(['docker', 'inspect', imageId], { quiet: true }); - const parsed = JSON.parse(manifest)[0]; - - const importantParts = { - Parent: parsed.Parent, - Comment: parsed.Comment, - Config: parsed.Config, - RootFS: parsed.RootFS, - Architecture: parsed.Architecture, - Os: parsed.Os, - Size: parsed.Size, - VirtualSize: parsed.VirtualSize, - GraphDriver: parsed.GraphDriver - }; - - return crypto.createHash('sha256').update(JSON.stringify(importantParts)).digest('hex'); +async function calculateImageFingerprint(imageId: string) { + const manifestString = await shell(['docker', 'inspect', imageId], { quiet: true }); + const manifest = JSON.parse(manifestString)[0]; + + // Id can change + delete manifest.Id; + + // Repository-based identifiers are out + delete manifest.RepoTags; + delete manifest.RepoDigests; + + // Metadata that has no bearing on the image contents + delete manifest.Created; + + // We're interested in the image itself, not any running instaces of it + delete manifest.Container; + delete manifest.ContainerConfig; + + // We're not interested in the Docker version used to create this image + delete manifest.DockerVersion; + + return crypto.createHash('sha256').update(JSON.stringify(manifest)).digest('hex'); } /** diff --git a/packages/aws-cdk/lib/logging.ts b/packages/aws-cdk/lib/logging.ts index d9b6ef87a2900..03405da3fdb1b 100644 --- a/packages/aws-cdk/lib/logging.ts +++ b/packages/aws-cdk/lib/logging.ts @@ -3,7 +3,7 @@ import util = require('util'); // tslint:disable:no-console the whole point of those methods is precisely to output to the console... -export let isVerbose = false; +let isVerbose = false; export function setVerbose(enabled = true) { isVerbose = enabled; diff --git a/packages/aws-cdk/lib/os.ts b/packages/aws-cdk/lib/os.ts index f12168bba8f8f..7fd8c56b6317d 100644 --- a/packages/aws-cdk/lib/os.ts +++ b/packages/aws-cdk/lib/os.ts @@ -93,6 +93,6 @@ function windowsEscape(x: string): string { // First surround by double quotes, ignore the part about backslashes x = `"${x}"`; // Now escape all special characters - const shellMeta = ['"', '&', '^', '%']; - return x.split('').map(c => shellMeta.indexOf(x) !== -1 ? '^' + c : c).join(''); + const shellMeta = new Set(['"', '&', '^', '%']); + return x.split('').map(c => shellMeta.has(x) ? '^' + c : c).join(''); } \ No newline at end of file diff --git a/packages/aws-cdk/lib/util/please-hold.ts b/packages/aws-cdk/lib/util/please-hold.ts index 044ff00259a6a..bc3f7185fd79a 100644 --- a/packages/aws-cdk/lib/util/please-hold.ts +++ b/packages/aws-cdk/lib/util/please-hold.ts @@ -1,4 +1,5 @@ -import { warning } from "../logging"; +import colors = require('colors/safe'); +import { print } from "../logging"; /** * Print a message to the logger in case the operation takes a long time @@ -20,6 +21,6 @@ export class PleaseHold { } private printMessage() { - warning(this.message); + print(colors.yellow(this.message)); } } \ No newline at end of file From 9bea6bee8eb8a729749a464346376f6c6a1b1153 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 24 Oct 2018 15:53:38 -0700 Subject: [PATCH 089/140] Add ability to specify cluster size Sets maxSize and desiredCapabity on ASG for ECS Cluster. NOTE: Should we set minSize to 0 or 1? See ecs-cli default) --- .../cdk-examples-typescript/hello-cdk-ecs/index.ts | 7 ++++++- packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 8d022e90561b5..7d823061cd580 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -1,4 +1,5 @@ import ec2 = require('@aws-cdk/aws-ec2'); +import { InstanceType } from '@aws-cdk/aws-ec2'; import ecs = require('@aws-cdk/aws-ecs'); import cdk = require('@aws-cdk/cdk'); @@ -11,7 +12,11 @@ class BonjourECS extends cdk.Stack { // deploy, but VPC creation is slow so we'll only have to do that once // and can iterate quickly on consuming stacks. Not doing that for now. const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); - const cluster = new ecs.EcsCluster(this, 'EcsCluster', { vpc }); + const cluster = new ecs.EcsCluster(this, 'EcsCluster', { + vpc, + size: 3, + instanceType: new InstanceType("t2.xlarge") + }); // Instantiate ECS Service with just cluster and image const ecsService = new ecs.LoadBalancedEcsService(this, "EcsService", { diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index aea6ba2d3b53e..be5e41cbefd67 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -17,6 +17,13 @@ export interface EcsClusterProps extends BaseClusterProps { * The type of EC2 instance to launch into your Autoscaling Group */ instanceType?: ec2.InstanceType; + + /** + * Number of container instances registered in your ECS Cluster + * + * @default 1 + */ + size?: number; } export class EcsCluster extends BaseCluster implements IEcsCluster { @@ -34,7 +41,10 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { vpc: props.vpc, instanceType: props.instanceType || new ec2.InstanceTypePair(ec2.InstanceClass.T2, ec2.InstanceSize.Micro), machineImage: new EcsOptimizedAmi(), - updateType: autoscaling.UpdateType.ReplacingUpdate + updateType: autoscaling.UpdateType.ReplacingUpdate, + minSize: 0, // NOTE: This differs from default of 1 in ASG construct lib -- also does not appear to work? + maxSize: props.size || 1, + desiredCapacity: props.size || 1 }); this.securityGroup = autoScalingGroup.connections.securityGroup!; @@ -171,4 +181,4 @@ class ImportedEcsCluster extends cdk.Construct implements IEcsCluster { this.vpc = ec2.VpcNetworkRef.import(this, "vpc", props.vpc); this.securityGroup = ec2.SecurityGroupRef.import(this, "securityGroup", props.securityGroup); } -} \ No newline at end of file +} From be9f4471bacab69f9c90de72eb7ce716075e9d51 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 26 Oct 2018 11:05:10 +0200 Subject: [PATCH 090/140] feat(aws-ecs): add Task AutoScaling to Service This uses the @aws-cdk/aws-applicationautoscaling library to add Task AutoScaling facilities to ECS and Fargate services. --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 44 +++++++- .../aws-ecs/lib/base/scalable-task-count.ts | 103 ++++++++++++++++++ .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 2 +- .../aws-ecs/lib/fargate/fargate-service.ts | 2 +- packages/@aws-cdk/aws-ecs/lib/index.ts | 21 ++-- packages/@aws-cdk/aws-ecs/package.json | 1 + .../@aws-cdk/aws-ecs/test/fargate/cdk.json | 16 +++ .../test/fargate/integ.lb-awsvpc-nw.ts | 4 + 8 files changed, 179 insertions(+), 14 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 81ef233c2d12e..52e9ded54fa20 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -1,9 +1,12 @@ +import appscaling = require('@aws-cdk/aws-applicationautoscaling'); import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; +import { ScalableTaskCount } from './scalable-task-count'; export interface BaseServiceProps { /** @@ -60,14 +63,17 @@ export abstract class BaseService extends cdk.Construct implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { public readonly dependencyElements: cdk.IDependable[]; public abstract readonly connections: ec2.Connections; + public readonly serviceArn: string; public readonly serviceName: string; + public readonly clusterName: string; protected loadBalancers = new Array(); protected networkConfiguration?: cloudformation.ServiceResource.NetworkConfigurationProperty; protected readonly abstract taskDef: BaseTaskDefinition; protected _securityGroup?: ec2.SecurityGroupRef; private readonly resource: cloudformation.ServiceResource; + private scalableTaskCount?: ScalableTaskCount; - constructor(parent: cdk.Construct, name: string, props: BaseServiceProps, additionalProps: any) { + constructor(parent: cdk.Construct, name: string, props: BaseServiceProps, additionalProps: any, clusterName: string) { super(parent, name); this.resource = new cloudformation.ServiceResource(this, "Service", { @@ -83,9 +89,10 @@ export abstract class BaseService extends cdk.Construct platformVersion: props.platformVersion, ...additionalProps }); - - this.dependencyElements = [this.resource]; + this.serviceArn = this.resource.serviceArn; this.serviceName = this.resource.serviceName; + this.dependencyElements = [this.resource]; + this.clusterName = clusterName; } /** @@ -111,6 +118,23 @@ export abstract class BaseService extends cdk.Construct return this._securityGroup!; } + /** + * Enable autoscaling for the number of tasks in this service + */ + public autoScaleTaskCount(props: appscaling.EnableScalingProps) { + if (this.scalableTaskCount) { + throw new Error('AutoScaling of task count already enabled for this service'); + } + + return this.scalableTaskCount = new ScalableTaskCount(this, 'TaskCount', { + serviceNamespace: appscaling.ServiceNamespace.Ecs, + resourceId: `service/${this.clusterName}/${this.resource.serviceName}`, + dimension: 'ecs:service:DesiredCount', + role: this.makeAutoScalingRole(), + ...props + }); + } + /** * Return the given named metric for this Service */ @@ -159,6 +183,20 @@ export abstract class BaseService extends cdk.Construct const targetType = this.taskDef.networkMode === NetworkMode.AwsVpc ? elbv2.TargetType.Ip : elbv2.TargetType.Instance; return { targetType }; } + + /** + * Generate the role that will be used for autoscaling this service + */ + private makeAutoScalingRole(): iam.IRole { + // Use a Service Linked Role. + return iam.Role.import(this, 'ScalingRole', { + roleArn: cdk.ArnUtils.fromComponents({ + service: 'iam', + resource: 'role/aws-service-role/ecs.application-autoscaling.amazonaws.com', + resourceName: 'AWSServiceRoleForApplicationAutoScaling_ECSService', + }) + }); + } } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts b/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts new file mode 100644 index 0000000000000..f63e42a18d1a3 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts @@ -0,0 +1,103 @@ +import appscaling = require('@aws-cdk/aws-applicationautoscaling'); +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); + +/** + * Scalable attribute representing task count + */ +export class ScalableTaskCount extends appscaling.BaseScalableAttribute { + /** + * Scale out or in based on time + */ + public scaleOnSchedule(id: string, props: appscaling.ScalingSchedule) { + return super.scaleOnSchedule(id, props); + } + + /** + * Scale out or in based on a metric value + */ + public scaleOnMetric(id: string, props: appscaling.BasicStepScalingPolicyProps) { + return super.scaleOnMetric(id, props); + } + + /** + * Scale out or in to achieve a target CPU utilization + */ + public scaleOnCpuUtilization(id: string, props: CpuUtilizationScalingProps) { + return super.scaleToTrackMetric(id, { + predefinedMetric: appscaling.PredefinedMetric.ECSServiceAverageCPUUtilization, + policyName: props.policyName, + disableScaleIn: props.disableScaleIn, + targetValue: props.targetUtilizationPercent, + scaleInCooldownSec: props.scaleInCooldownSec, + scaleOutCooldownSec: props.scaleOutCooldownSec, + }); + } + + /** + * Scale out or in to achieve a target memory utilization utilization + */ + public scaleOnMemoryUtilization(id: string, props: CpuUtilizationScalingProps) { + return super.scaleToTrackMetric(id, { + predefinedMetric: appscaling.PredefinedMetric.ECSServiceAverageMemoryUtilization, + targetValue: props.targetUtilizationPercent, + policyName: props.policyName, + disableScaleIn: props.disableScaleIn, + scaleInCooldownSec: props.scaleInCooldownSec, + scaleOutCooldownSec: props.scaleOutCooldownSec, + }); + } + + /** + * Scale out or in to track a custom metric + */ + public scaleToTrackCustomMetric(id: string, props: TrackCustomMetricProps) { + return super.scaleToTrackMetric(id, { + customMetric: props.metric, + targetValue: props.targetValue, + policyName: props.policyName, + disableScaleIn: props.disableScaleIn, + scaleInCooldownSec: props.scaleInCooldownSec, + scaleOutCooldownSec: props.scaleOutCooldownSec, + }); + } +} + +/** + * Properties for enabling scaling based on CPU utilization + */ +export interface CpuUtilizationScalingProps extends appscaling.BaseTargetTrackingProps { + /** + * Target average CPU utilization across the task + */ + targetUtilizationPercent: number; +} + +/** + * Properties for enabling scaling based on memory utilization + */ +export interface MemoryUtilizationScalingProps extends appscaling.BaseTargetTrackingProps { + /** + * Target average memory utilization across the task + */ + targetUtilizationPercent: number; +} + +/** + * Properties to target track a custom metric + */ +export interface TrackCustomMetricProps extends appscaling.BaseTargetTrackingProps { + /** + * Metric to track + * + * The metric must represent utilization; that is, you will always get the following behavior: + * + * - metric > targetValue => scale out + * - metric < targetValue => scale in + */ + metric: cloudwatch.Metric; + + /** + * The target value to achieve for the metric + */ + targetValue: number; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index 2821d23c8da27..f0c6c1a80abe1 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -76,7 +76,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { placementConstraints: new cdk.Token(() => this.constraints), placementStrategies: new cdk.Token(() => this.strategies), schedulingStrategy: props.daemon ? 'DAEMON' : 'REPLICA', - }); + }, props.cluster.clusterName); this.clusterName = props.cluster.clusterName; this.constraints = []; diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 8144ae913607e..fa622fd71df98 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -48,7 +48,7 @@ export class FargateService extends BaseService { cluster: props.cluster.clusterName, taskDefinition: props.taskDefinition.taskDefinitionArn, launchType: 'FARGATE', - }); + }, props.cluster.clusterName); this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcPlacement, props.securityGroup); this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index b163abbafa24b..4031b3a6078fc 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,23 +1,26 @@ export * from './base/base-cluster'; -export * from './ecs/ecs-cluster'; -export * from './fargate/fargate-cluster'; - export * from './base/base-service'; -export * from './ecs/ecs-service'; -export * from './fargate/fargate-service'; - export * from './base/base-task-definition'; -export * from './ecs/ecs-task-definition'; -export * from './fargate/fargate-task-definition'; +export * from './base/scalable-task-count'; + export * from './container-definition'; export * from './container-image'; + +export * from './ecs/ecs-cluster'; +export * from './ecs/ecs-service'; +export * from './ecs/ecs-task-definition'; + +export * from './fargate/fargate-cluster'; +export * from './fargate/fargate-service'; +export * from './fargate/fargate-task-definition'; + export * from './linux-parameters'; export * from './load-balanced-ecs-service'; export * from './load-balanced-fargate-service'; export * from './load-balanced-fargate-service-applet'; -export * from './log-drivers/log-driver'; export * from './log-drivers/aws-log-driver'; +export * from './log-drivers/log-driver'; // AWS::ECS CloudFormation Resources: // diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 1374f28487a85..07f57276c30c9 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -58,6 +58,7 @@ "pkglint": "^0.13.0" }, "dependencies": { + "@aws-cdk/aws-applicationautoscaling": "^0.13.0", "@aws-cdk/aws-autoscaling": "^0.13.0", "@aws-cdk/aws-cloudwatch": "^0.13.0", "@aws-cdk/aws-ec2": "^0.13.0", diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json index 64fd3b2dafe6e..be30e8b227170 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/cdk.json @@ -4,6 +4,22 @@ "eu-west-1a", "eu-west-1b", "eu-west-1c" + ], + "availability-zones:993655754359:us-east-1": [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f" + ], + "availability-zones:435784268886:us-east-1": [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f" ] } } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index 87ed17d1bab93..b3c6f9401d46a 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -29,6 +29,10 @@ const service = new ecs.FargateService(stack, "Service", { taskDefinition, }); +const scaling = service.autoScaleTaskCount({ maxCapacity: 10 }); +// Quite low to try and force it to scale +scaling.scaleOnCpuUtilization('ReasonableCpu', { targetUtilizationPercent: 10 }); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); const listener = lb.addListener('PublicListener', { port: 80, open: true }); listener.addTargets('Fargate', { From 2eab5135066a9d4db8b17e268e57b998bc8874b9 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Fri, 26 Oct 2018 13:02:39 -0700 Subject: [PATCH 091/140] Add unit tests for EcsCluster --- .../aws-ecs/test/ecs/test.ecs-cluster.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts new file mode 100644 index 0000000000000..a427c54577892 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts @@ -0,0 +1,45 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../../lib'); + +export = { + "When creating an ECS Cluster": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + new ecs.EcsCluster(stack, 'Cluster', { + vpc, + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Cluster', { + // Type: "AWS::EC2::VPC", + })); + // Properties: { + // CidrBlock: '10.10.0.0/16', + // EnableDnsHostnames: true, + // EnableDnsSupport: true, + // InstanceTenancy: ec2.DefaultInstanceTenancy.Default, + // Tags: [ + // { + // Key: "Name", + // Value: "MyVpc" + // } + // ] + // } + // })); + // expect(stack).toMatch({ + // Resources: { + // Cluster: { + // Type: 'AWS::ECS::Cluster' + // } + // } + // }, MatchStyle.SUPERSET);​ + + test.done(); + }, + } +}; From 4cf23fc23c0f2fe0a97569814925eed2cce30de4 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 29 Oct 2018 11:18:34 -0700 Subject: [PATCH 092/140] Unit tests for Cluster --- .../@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 4 +- .../aws-ecs/test/ecs/test.ecs-cluster.ts | 200 +++++++++++++++--- .../test/fargate/test.fargate-cluster.ts | 41 ++++ 3 files changed, 218 insertions(+), 27 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-cluster.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index 2f9be92c8587f..577169eaceb2c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -42,7 +42,7 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { instanceType: props.instanceType || new ec2.InstanceTypePair(ec2.InstanceClass.T2, ec2.InstanceSize.Micro), machineImage: new EcsOptimizedAmi(), updateType: autoscaling.UpdateType.ReplacingUpdate, - minSize: 0, // NOTE: This differs from default of 1 in ASG construct lib -- also does not appear to work? + minSize: 0, maxSize: props.size || 1, desiredCapacity: props.size || 1 }); @@ -97,6 +97,8 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { this.autoScalingGroup = autoScalingGroup; } + // TODO Add cluster scaling API + /** * Export the EcsCluster */ diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts index a427c54577892..ccf2801f83f11 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts @@ -1,5 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); +import { InstanceType } from '@aws-cdk/aws-ec2'; import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import ecs = require('../../lib'); @@ -10,36 +11,183 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - new ecs.EcsCluster(stack, 'Cluster', { + new ecs.EcsCluster(stack, 'EcsCluster', { vpc, }); - // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { - // Type: "AWS::EC2::VPC", + expect(stack).to(haveResource("AWS::ECS::Cluster")); + + expect(stack).to(haveResource("AWS::EC2::VPC", { + CidrBlock: '10.0.0.0/16', + EnableDnsHostnames: true, + EnableDnsSupport: true, + InstanceTenancy: ec2.DefaultInstanceTenancy.Default, + Tags: [ + { + Key: "Name", + Value: "MyVpc" + } + ] + })); + + expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", { + ImageId: "", // Should this not be the latest image ID? + InstanceType: "t2.micro", + IamInstanceProfile: { + Ref: "EcsClusterAutoScalingGroupInstanceProfile77D897B8" + }, + SecurityGroups: [ + { + "Fn::GetAtt": [ + "EcsClusterAutoScalingGroupInstanceSecurityGroupBFB09B50", + "GroupId" + ] + } + ], + UserData: { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + Ref: "EcsCluster97242B84" + }, + // tslint:disable-next-line:max-line-length + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save" + ] + ] + } + } + })); + + expect(stack).to(haveResource("AWS::AutoScaling::AutoScalingGroup", { + MaxSize: "1", + MinSize: "0", + DesiredCapacity: "1", + LaunchConfigurationName: { + Ref: "EcsClusterAutoScalingGroupLaunchConfig965E00BD" + }, + Tags: [ + { + Key: "Name", + PropagateAtLaunch: true, + Value: "EcsCluster/AutoScalingGroup" + } + ], + VPCZoneIdentifier: [ + { + Ref: "MyVpcPrivateSubnet1Subnet5057CF7E" + }, + { + Ref: "MyVpcPrivateSubnet2Subnet0040C983" + }, + { + Ref: "MyVpcPrivateSubnet3Subnet772D6AD7" + } + ] + })); + + expect(stack).to(haveResource("AWS::EC2::SecurityGroup", { + GroupDescription: "EcsCluster/AutoScalingGroup/InstanceSecurityGroup", + SecurityGroupEgress: [ + { + CidrIp: "0.0.0.0/0", + Description: "Allow all outbound traffic by default", + IpProtocol: "-1" + } + ], + SecurityGroupIngress: [], + Tags: [ + { + Key: "Name", + Value: "EcsCluster/AutoScalingGroup" + } + ], + VpcId: { + Ref: "MyVpcF9F0CA6F" + } + })); + + expect(stack).to(haveResource("AWS::IAM::Role", { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "ec2.amazonaws.com" + } + } + ], + Version: "2012-10-17" + } + })); + + expect(stack).to(haveResource("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Effect: "Allow", + Resource: "*" + } + ], + Version: "2012-10-17" + } })); - // Properties: { - // CidrBlock: '10.10.0.0/16', - // EnableDnsHostnames: true, - // EnableDnsSupport: true, - // InstanceTenancy: ec2.DefaultInstanceTenancy.Default, - // Tags: [ - // { - // Key: "Name", - // Value: "MyVpc" - // } - // ] - // } - // })); - // expect(stack).toMatch({ - // Resources: { - // Cluster: { - // Type: 'AWS::ECS::Cluster' - // } - // } - // }, MatchStyle.SUPERSET);​ test.done(); }, - } -}; + }, + + "allows specifying instance type"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + + new ecs.EcsCluster(stack, 'EcsCluster', { + vpc, + instanceType: new InstanceType("m3.large") + }); + + // THEN + expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", { + InstanceType: "m3.large" + })); + + test.done(); + }, + + "allows specifying cluster size"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + + new ecs.EcsCluster(stack, 'EcsCluster', { + vpc, + size: 3 + }); + + // THEN + expect(stack).to(haveResource("AWS::AutoScaling::AutoScalingGroup", { + MaxSize: "3" + })); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-cluster.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-cluster.ts new file mode 100644 index 0000000000000..f25bcaf95985e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-cluster.ts @@ -0,0 +1,41 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../../lib'); + +export = { + "When creating a Fargate Cluster": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + new ecs.FargateCluster(stack, 'FargateCluster', { + vpc, + }); + + expect(stack).to(haveResource("AWS::ECS::Cluster")); + + expect(stack).to(haveResource("AWS::EC2::VPC", { + CidrBlock: '10.0.0.0/16', + EnableDnsHostnames: true, + EnableDnsSupport: true, + InstanceTenancy: ec2.DefaultInstanceTenancy.Default, + Tags: [ + { + Key: "Name", + Value: "MyVpc" + } + ] + })); + + expect(stack).notTo(haveResource("AWS::EC2::SecurityGroup")); + expect(stack).notTo(haveResource("AWS::AutoScaling::LaunchConfiguration")); + expect(stack).notTo(haveResource("AWS::AutoScaling::AutoScalingGroup")); + expect(stack).notTo(haveResource("AWS::IAM::Role")); + expect(stack).notTo(haveResource("AWS::IAM::Policy")); + + test.done(); + }, + } +}; From 37d3c4e863e0faae3577885727016f4cb5462fdd Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 29 Oct 2018 19:11:16 -0700 Subject: [PATCH 093/140] Set volumes on TaskDefinition --- packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index d56e17cb3c61d..3c253973275af 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -69,6 +69,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { containerDefinitions: new cdk.Token(() => this.containers.map(x => x.renderContainerDefinition())), + volumes: new cdk.Token(() => this.volumes), executionRoleArn: new cdk.Token(() => this.executionRole && this.executionRole.roleArn), family: this.family, taskRoleArn: this.taskRole.roleArn, @@ -87,6 +88,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { /** * Add a container to this task + * FIXME pass in actual container instead of container props? */ public addContainer(id: string, props: ContainerDefinitionProps) { const container = new ContainerDefinition(this, id, this, props); @@ -102,7 +104,6 @@ export abstract class BaseTaskDefinition extends cdk.Construct { } private addVolume(volume: Volume) { - // const v = this.renderVolume(volume); this.volumes.push(volume); } @@ -152,9 +153,11 @@ export enum Compatibilities { Fargate = "FARGATE" } +// FIXME separate Volume from InstanceVolume (Host not supported in Fargate) export interface Volume { host?: Host; name?: string; + // FIXME add dockerVolumeConfiguration } export interface Host { From 1ad3669f7e34ed0389e6504025536ba2a1653adf Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 29 Oct 2018 22:38:45 -0700 Subject: [PATCH 094/140] Clarify doc string for memoryLimit and memoryReservation --- packages/@aws-cdk/aws-ecs/lib/container-definition.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index eac5e4935a8d5..d5820a2169e39 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -119,7 +119,7 @@ export interface ContainerDefinitionProps { * If your container attempts to exceed the allocated memory, the container * is terminated. * - * At least one of memoryLimitMiB and memoryReservationMiB is required. + * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. */ memoryLimitMiB?: number; @@ -131,7 +131,7 @@ export interface ContainerDefinitionProps { * it can consume up to the value specified by the Memory property or all of * the available memory on the container instance—whichever comes first. * - * At least one of memoryLimitMiB and memoryReservationMiB is required. + * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. */ memoryReservationMiB?: number; @@ -302,6 +302,7 @@ export class ContainerDefinition extends cdk.Construct { healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), links: this.links, linuxParameters: this.linuxParameters.renderLinuxParameters(), + }; } } From 78d2a49597fe7dd687b628d95f61fa3ec0fd2adc Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 29 Oct 2018 19:11:53 -0700 Subject: [PATCH 095/140] WIP Unit tests on EcsTaskDefinition TaskExecutionRole not being set -- lazy evaluation of executionRole on TaskDef not working on private call to generateExecutionRole? Also weird issue with importing aws-iam --- .../test/ecs/test.ecs-task-definition.ts | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts new file mode 100644 index 0000000000000..d5a3b2b7c0da3 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts @@ -0,0 +1,209 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Protocol } from '@aws-cdk/aws-ec2'; +// import iam = require('@aws-cdk/aws-iam'); // importing this is throwing a really weird error in line 11? +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../../lib'); + +export = { + "When creating an ECS TaskDefinition": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + Family: "EcsTaskDef", + ContainerDefinitions: [], + PlacementConstraints: [], + Volumes: [], + NetworkMode: ecs.NetworkMode.Bridge, + RequiresCompatibilities: [ecs.Compatibilities.Ec2] + })); + + // test error if no container defs? + test.done(); + }, + + "correctly sets network mode"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + networkMode: ecs.NetworkMode.AwsVpc + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + NetworkMode: ecs.NetworkMode.AwsVpc, + })); + + test.done(); + }, + + "correctly sets containers"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + const container = taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 // add validation? + }); + + // TODO test other containerDefinition methods + container.addPortMappings({ + containerPort: 3000 + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + Family: "EcsTaskDef", + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: "amazon/amazon-ecs-sample", + Links: [], + LinuxParameters: { + Capabilities: { + Add: [], + Drop: [] + }, + Devices: [], + Tmpfs: [] + }, + MountPoints: [], + Name: "web", + PortMappings: [{ + ContainerPort: 3000, + HostPort: 0, + Protocol: Protocol.Tcp + }], + Ulimits: [], + VolumesFrom: [] + }], + })); + + test.done(); + }, + + "correctly sets volumes"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const volume = { + host: { + sourcePath: "/tmp/cache", + }, + name: "scratch" + }; + + // Adding volumes via props is a bit clunky + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + volumes: [volume] + }); + + const container = taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + // this needs to be a better API -- should auto-add volumes + container.addMountPoints({ + containerPath: "./cache", + readOnly: true, + sourceVolume: "scratch", + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + Family: "EcsTaskDef", + ContainerDefinitions: [{ + MountPoints: [ + { + ContainerPath: "./cache", + ReadOnly: true, + SourceVolume: "scratch" + } + ] + }], + Volumes: [{ + Host: { + SourcePath: "/tmp/cache" + }, + Name: "scratch" + }] + })); + + test.done(); + }, + + "correctly sets placement constraints"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + placementConstraints: [{ + expression: "attribute:ecs.instance-type =~ t2.*", + type: ecs.PlacementConstraintType.MemberOf + }] + }); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample") + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + PlacementConstraints: [ + { + Expression: "attribute:ecs.instance-type =~ t2.*", + Type: "memberOf" + } + ] + })); + + test.done(); + }, + + // "correctly sets taskRole"(test: Test) { + // // GIVEN + // const stack = new cdk.Stack(); + // const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + // taskRole: new iam.Role(this, 'TaskRole', { + // assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + // }) + // }); + + // taskDefinition.addContainer("web", { + // image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + // memoryLimitMiB: 512 + // }); + + // // THEN + // expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + // TaskRole: "roleArn" + // })); + + // test.done(); + // }, + + // "correctly sets taskExecutionRole if containerDef uses ECR"(test: Test) { + // // GIVEN + // const stack = new cdk.Stack(); + // const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', {}); + // const container = taskDefinition.addContainer("web", { + // image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + // memoryLimitMiB: 512 // add validation? + // }); + + // container.useEcrImage(); + + // // THEN + // expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + // TaskExecutionRole: "roleArn" + // })); + + // test.done(); + // }, + } +}; \ No newline at end of file From ca61a7473ae0c18e70f0460f85871d88772b7c94 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 29 Oct 2018 23:29:36 -0700 Subject: [PATCH 096/140] WIP Unit tests for EcsService Add default deployment configuration --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 6 +- .../aws-ecs/test/ecs/test.ecs-service.ts | 391 ++++++++++++++++++ 2 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 81ef233c2d12e..2bbc3799c317d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -41,7 +41,7 @@ export interface BaseServiceProps { /** * Time after startup to ignore unhealthy load balancer checks. * - * @default ??? + * @default ??? FIXME */ healthCheckGracePeriodSeconds?: number; @@ -75,8 +75,8 @@ export abstract class BaseService extends cdk.Construct serviceName: props.serviceName, loadBalancers: new cdk.Token(() => this.loadBalancers), deploymentConfiguration: { - maximumPercent: props.maximumPercent, - minimumHealthyPercent: props.minimumHealthyPercent + maximumPercent: props.maximumPercent || 200, + minimumHealthyPercent: props.minimumHealthyPercent || 50 }, /* role: never specified, supplanted by Service Linked Role */ networkConfiguration: new cdk.Token(() => this.networkConfiguration), diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts new file mode 100644 index 0000000000000..3b2c29c76065f --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts @@ -0,0 +1,391 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../../lib'); +import { BinPackResource, BuiltInAttributes, NetworkMode } from '../../lib'; + +export = { + "When creating an ECS Service": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + TaskDefinition: { + Ref: "EcsTaskDefA3440FB6" + }, + Cluster: { + Ref: "EcsCluster97242B84" + }, + DeploymentConfiguration: { + MaximumPercent: 200, + MinimumHealthyPercent: 50 + }, + DesiredCount: 1, + LaunchType: "EC2", + LoadBalancers: [], + PlacementConstraints: [], + PlacementStrategies: [], + SchedulingStrategy: "REPLICA" + })); + + test.done(); + }, + + "errors if daemon and desiredCount both specified"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + // THEN + test.throws(() => { + new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + daemon: true, + desiredCount: 2 + }); + }); + + test.done(); + }, + + "errors if no container definitions"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + // THEN + test.throws(() => { + new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + }); + }); + + test.done(); + }, + + "sets daemon scheduling strategy"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + daemon: true + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + SchedulingStrategy: "DAEMON" + })); + + test.done(); + }, + + "with a TaskDefinition with AwsVpc network mode": { + "it creates a security group for the service"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + networkMode: NetworkMode.AwsVpc + }); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + NetworkConfiguration: { + AwsvpcConfiguration: { + AssignPublicIp: "DISABLED", + SecurityGroups: [ + { + "Fn::GetAtt": [ + "EcsServiceSecurityGroup8FDFD52F", + "GroupId" + ] + } + ], + Subnets: [ + { + Ref: "MyVpcPrivateSubnet1Subnet5057CF7E" + }, + { + Ref: "MyVpcPrivateSubnet2Subnet0040C983" + }, + { + Ref: "MyVpcPrivateSubnet3Subnet772D6AD7" + } + ] + } + } + })); + + test.done(); + } + }, + + "with distinctInstance placement constraint"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + placeOnDistinctInstances: true + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + PlacementConstraints: [{ + Type: "distinctInstance" + }] + })); + + test.done(); + }, + + "with memberOf placement constraints"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition + }); + + service.placeOnMemberOf("attribute:ecs.instance-type =~ t2.*"); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + PlacementConstraints: [{ + Expression: "attribute:ecs.instance-type =~ t2.*", + Type: "memberOf" + }] + })); + + test.done(); + }, + + "with placeSpreadAcross placement strategy"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition + }); + + service.placeSpreadAcross(BuiltInAttributes.AvailabilityZone); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + PlacementStrategies: [{ + Field: "attribute:ecs.availability-zone", + Type: "spread" + }] + })); + + test.done(); + }, + + "errors with placeSpreadAcross placement strategy if daemon specified"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + daemon: true + }); + + // THEN + test.throws(() => { + service.placeSpreadAcross(BuiltInAttributes.AvailabilityZone); + }); + + test.done(); + }, + + "with placeRandomly placement strategy"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition + }); + + service.placeRandomly(); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + PlacementStrategies: [{ + Type: "random" + }] + })); + + test.done(); + }, + + "errors with placeRandomly placement strategy if daemon specified"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + daemon: true + }); + + // THEN + test.throws(() => { + service.placeRandomly(); + }); + + test.done(); + }, + + "with placePackedBy placement strategy"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition + }); + + service.placePackedBy(BinPackResource.Memory); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + PlacementStrategies: [{ + Field: "memory", + Type: "binpack" + }] + })); + + test.done(); + }, + + "errors with placePackedBy placement strategy if daemon specified"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.EcsService(stack, "EcsService", { + cluster, + taskDefinition, + daemon: true + }); + + // THEN + test.throws(() => { + service.placePackedBy(BinPackResource.Memory); + }); + + test.done(); + } + } +}; \ No newline at end of file From 98345b3a6ee8f1934a6b60713b4e7f100e928dfc Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 30 Oct 2018 01:03:19 -0700 Subject: [PATCH 097/140] WIP Unit tests for FargateService --- .../aws-ecs/lib/fargate/fargate-service.ts | 2 +- .../test/fargate/test.fargate-service.ts | 114 ++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 8144ae913607e..b000237d689d2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -26,7 +26,7 @@ export interface FargateServiceProps extends BaseServiceProps { /** * In what subnets to place the task's ENIs * - * @default Public subnet if assignPublicIp, private subnets otherwise + * @default Private subnet if assignPublicIp, public subnets otherwise */ vpcPlacement?: ec2.VpcPlacementStrategy; diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts new file mode 100644 index 0000000000000..9eaa639f394bf --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -0,0 +1,114 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../../lib'); + +export = { + "When creating a Fargate Service": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.FargateCluster(stack, 'FargateCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + }); + + new ecs.FargateService(stack, "FargateService", { + cluster, + taskDefinition + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + TaskDefinition: { + Ref: "FargateTaskDefC6FB60B4" + }, + Cluster: { + Ref: "FargateCluster7CCD5F93" + }, + DeploymentConfiguration: { + MaximumPercent: 200, + MinimumHealthyPercent: 50 + }, + DesiredCount: 1, + LaunchType: "FARGATE", + LoadBalancers: [], + NetworkConfiguration: { + AwsvpcConfiguration: { + AssignPublicIp: "DISABLED", + SecurityGroups: [ + { + "Fn::GetAtt": [ + "FargateServiceSecurityGroup0A0E79CB", + "GroupId" + ] + } + ], + Subnets: [ + { + Ref: "MyVpcPrivateSubnet1Subnet5057CF7E" + }, + { + Ref: "MyVpcPrivateSubnet2Subnet0040C983" + }, + { + Ref: "MyVpcPrivateSubnet3Subnet772D6AD7" + } + ] + } + } + })); + + expect(stack).to(haveResource("AWS::EC2::SecurityGroup", { + GroupDescription: "FargateService/SecurityGroup", + SecurityGroupEgress: [ + { + CidrIp: "0.0.0.0/0", + Description: "Allow all outbound traffic by default", + IpProtocol: "-1" + } + ], + SecurityGroupIngress: [], + VpcId: { + Ref: "MyVpcF9F0CA6F" + } + })); + + test.done(); + }, + + "allows specifying assignPublicIP as enabled"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.FargateCluster(stack, 'FargateCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + }); + + new ecs.FargateService(stack, "FargateService", { + cluster, + taskDefinition, + assignPublicIp: true + + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + NetworkConfiguration: { + AwsvpcConfiguration: { + AssignPublicIp: "ENABLED", + } + } + })); + + test.done(); + }, + } +}; \ No newline at end of file From 35ebd9ca8e3785f5bb771f27ab9f7a947463601e Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 30 Oct 2018 01:44:04 -0700 Subject: [PATCH 098/140] WIP unit tests Fargate Task Definition --- .../fargate/test.fargate-task-definition.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts new file mode 100644 index 0000000000000..daff091baa022 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts @@ -0,0 +1,28 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../../lib'); + +export = { + "When creating an Fargate TaskDefinition": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + Family: "FargateTaskDef", + ContainerDefinitions: [], + Volumes: [], + NetworkMode: ecs.NetworkMode.AwsVpc, + RequiresCompatibilities: [ecs.Compatibilities.Fargate], + Cpu: "256", + Memory: "512", + })); + + // test error if no container defs? + test.done(); + }, + } +}; \ No newline at end of file From 7bbe9bb3b352904044a4141304229c1205fb5016 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 29 Oct 2018 17:05:44 +0100 Subject: [PATCH 099/140] feat(applets): integrate into toolkit Integrate the Applet runner into the toolkit. When the --app argument points to a .yml or .yaml file, the target will be assumed to be a JavaScript applet, and run through the applet interpreter. Enhancements to JS applet interpreter: - Allow using non-Stack constructs. - Multiple Stacks in one applet file by supplying an array. - Allow referencing packages directly from NPM. Non-Stack constructs can now be used as applets. If a non-Stack applet is selected, a Stack will be constructed around it. This makes it possible to reuse Applet constructs in regular CDK applications (if they meet the requirements). Multiple applets can be define in one file by making the top-level object in the YAML file a list. Each applet will get its own stack. By starting an applet specifier with npm://, applet modules can directly be referenced in NPM. You can include a version specifier (@1.2.3) to reference specific versions. --- .../@aws-cdk/applet-js/bin/cdk-applet-js.ts | 96 +- .../@aws-cdk/applet-js/lib/applet-helpers.ts | 52 + packages/@aws-cdk/applet-js/package-lock.json | 5511 ++++++++++++++++- packages/@aws-cdk/applet-js/package.json | 2 +- .../@aws-cdk/applet-js/test/expected1.json | 66 +- .../@aws-cdk/applet-js/test/expected2.json | 88 +- .../@aws-cdk/applet-js/test/expected3.json | 16 +- .../applet-js/test/manual-test-npm.yaml | 1 + .../applet-js/test/strip-stacktrace.ts | 26 - .../@aws-cdk/applet-js/test/test-applet.ts | 14 +- .../@aws-cdk/applet-js/test/test-fromnpm.yaml | 1 + .../test/test-multistack-expected.json | 35 + .../applet-js/test/test-multistack.yaml | 9 + .../test/test-nonstack-expected.json | 22 + .../applet-js/test/test-nonstack.yaml | 4 + .../@aws-cdk/applet-js/test/test.applets.ts | 172 + .../@aws-cdk/applet-js/test/test.helpers.ts | 56 + packages/aws-cdk/bin/cdk.ts | 144 +- packages/aws-cdk/lib/exec.ts | 154 + packages/aws-cdk/lib/settings.ts | 3 + packages/aws-cdk/package.json | 1 + 21 files changed, 6130 insertions(+), 343 deletions(-) create mode 100644 packages/@aws-cdk/applet-js/lib/applet-helpers.ts create mode 100644 packages/@aws-cdk/applet-js/test/manual-test-npm.yaml delete mode 100644 packages/@aws-cdk/applet-js/test/strip-stacktrace.ts create mode 100644 packages/@aws-cdk/applet-js/test/test-fromnpm.yaml create mode 100644 packages/@aws-cdk/applet-js/test/test-multistack-expected.json create mode 100644 packages/@aws-cdk/applet-js/test/test-multistack.yaml create mode 100644 packages/@aws-cdk/applet-js/test/test-nonstack-expected.json create mode 100644 packages/@aws-cdk/applet-js/test/test-nonstack.yaml create mode 100644 packages/@aws-cdk/applet-js/test/test.applets.ts create mode 100644 packages/@aws-cdk/applet-js/test/test.helpers.ts create mode 100644 packages/aws-cdk/lib/exec.ts diff --git a/packages/@aws-cdk/applet-js/bin/cdk-applet-js.ts b/packages/@aws-cdk/applet-js/bin/cdk-applet-js.ts index 98a0fe4efdabe..c5d2a86fcd469 100644 --- a/packages/@aws-cdk/applet-js/bin/cdk-applet-js.ts +++ b/packages/@aws-cdk/applet-js/bin/cdk-applet-js.ts @@ -2,8 +2,11 @@ import 'source-map-support/register'; import cdk = require('@aws-cdk/cdk'); +import child_process = require('child_process'); import fs = require('fs-extra'); +import os = require('os'); import path = require('path'); +import { isStackConstructor, parseApplet } from '../lib/applet-helpers'; // tslint:disable-next-line:no-var-requires const YAML = require('js-yaml'); @@ -19,65 +22,84 @@ async function main() { const appletFile = process.argv[2]; if (!appletFile) { - throw new Error(`Usage: ${progname}| `); + throw new Error(`Usage: ${progname} `); } - // read applet properties from the provided file - const props = YAML.safeLoad(await fs.readFile(appletFile, { encoding: 'utf-8' })); + // read applet(s) properties from the provided file + let fileContents = YAML.safeLoad(await fs.readFile(appletFile, { encoding: 'utf-8' })); + if (!Array.isArray(fileContents)) { + fileContents = [fileContents]; + } + + const searchDir = path.dirname(appletFile); + const tempDir = await fs.mkdtemp(os.tmpdir()); + try { + + const app = new cdk.App(); + for (const props of fileContents) { + await constructStack(app, searchDir, tempDir, props); + } + app.run(); + } finally { + await fs.remove(tempDir); + } +} +/** + * Construct a stack from the given props + * @param props Const + */ +async function constructStack(app: cdk.App, searchDir: string, tempDir: string, props: any) { // the 'applet' attribute tells us how to load the applet. in the javascript case // it will be in the format : where is technically passed to "require" // and is expected to be exported from the module. - const applet: string = props.applet; - if (!applet) { + const appletSpec: string = props.applet; + if (!appletSpec) { throw new Error('Applet file missing "applet" attribute'); } - const { moduleName, className } = parseApplet(applet); + const applet = parseApplet(appletSpec); // remove the 'applet' attribute as we pass it along to the applet class. delete props.applet; + if (applet.npmPackage) { + // tslint:disable-next-line:no-console + console.error(`Installing NPM package ${applet.npmPackage}`); + // Magic marker to download this package directly off of NPM + // We're going to run NPM as a shell (since programmatic usage is not stable + // by their own admission) and we're installing into a temporary directory. + // (Installing into a permanent directory is useless since NPM doesn't do + // any real caching anyway). + child_process.execFileSync('npm', ['install', '--prefix', tempDir, '--global', applet.npmPackage], { + stdio: 'inherit' + }); + searchDir = path.join(tempDir, 'lib'); + } + // we need to resolve the module name relatively to where the applet file is // and not relative to this module or cwd. - const resolve = require.resolve as any; // escapse type-checking since { paths } is not defined - const modulePath = resolve(moduleName, { paths: [ path.dirname(appletFile) ] }); + const resolve = require.resolve as any; // escape type-checking since { paths } is not defined + const modulePath = resolve(applet.moduleName, { paths: [ searchDir ] }); // load the module const pkg = require(modulePath); // find the applet class within the package // tslint:disable-next-line:variable-name - const AppletStack = pkg[className]; - if (!AppletStack) { - throw new Error(`Cannot find applet class "${className}" in module "${moduleName}"`); + const appletConstructor = pkg[applet.className]; + if (!appletConstructor) { + throw new Error(`Cannot find applet class "${applet.className}" in module "${applet.moduleName}"`); } - // create the CDK app - const app = new cdk.App(); - - const constructName = props.name || className; - - // add the applet stack into the app. - new AppletStack(app, constructName, props); + const stackName = props.name || applet.className; - // transfer control to the app - app.run(); -} - -function parseApplet(applet: string) { - const components = applet.split(':'); - // tslint:disable-next-line:prefer-const - let [ moduleName, className ] = components; - - if (components.length > 2 || !moduleName) { - throw new Error(`"applet" value is "${applet}" but it must be in the form "[:]". - If is not specified, "Applet" is the default`); + if (isStackConstructor(appletConstructor)) { + // add the applet stack into the app. + new appletConstructor(app, stackName, props); + } else { + // Make a stack THEN add it in + const stack = new cdk.Stack(app, stackName, props); + new appletConstructor(stack, 'Default', props); } - - if (!className) { - className = 'Applet'; - } - - return { moduleName, className }; -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/applet-js/lib/applet-helpers.ts b/packages/@aws-cdk/applet-js/lib/applet-helpers.ts new file mode 100644 index 0000000000000..2bc519d12ad06 --- /dev/null +++ b/packages/@aws-cdk/applet-js/lib/applet-helpers.ts @@ -0,0 +1,52 @@ +/** + * Determine whether this constructorFunction is going to create an object that inherits from Stack + * + * We do structural typing. + */ +export function isStackConstructor(constructorFn: any) { + // Test for a public method that Stack has + return constructorFn.prototype.findResource !== undefined; +} + +/** + * Extract module name from a NPM package specification + */ +export function extractModuleName(packageSpec: string) { + const m = /^((?:@[a-zA-Z-]+\/)?[a-zA-Z-]+)/i.exec(packageSpec); + if (!m) { throw new Error(`Could not find package name in ${packageSpec}`); } + return m[1]; +} + +export function parseApplet(applet: string): AppletSpec { + const m = /^(npm:\/\/)?([a-z0-9_@./-]+)(:[a-z_0-9]+)?$/i.exec(applet); + if (!m) { + throw new Error(`"applet" value is "${applet}" but it must be in the form "[npm://][:]". + If is not specified, "Applet" is the default`); + } + + if (m[1] === 'npm://') { + return { + npmPackage: m[2], + moduleName: extractModuleName(m[2]), + className: className(m[3]), + }; + } else { + return { + moduleName: m[2], + className: className(m[3]), + }; + } + + function className(s: string | undefined) { + if (s) { + return s.substr(1); + } + return 'Applet'; + } +} + +export interface AppletSpec { + npmPackage?: string; + moduleName: string; + className: string; +} \ No newline at end of file diff --git a/packages/@aws-cdk/applet-js/package-lock.json b/packages/@aws-cdk/applet-js/package-lock.json index 5fbbaff225f51..f5a43d3c75c91 100644 --- a/packages/@aws-cdk/applet-js/package-lock.json +++ b/packages/@aws-cdk/applet-js/package-lock.json @@ -1,15 +1,43 @@ { "name": "@aws-cdk/applet-js", - "version": "0.12.0", + "version": "0.14.1", "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "7.0.0" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "2.4.1", + "esutils": "2.0.2", + "js-tokens": "4.0.0" + }, + "dependencies": { + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + } + } + }, "@types/fs-extra": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.4.tgz", "integrity": "sha512-DsknoBvD8s+RFfSGjmERJ7ZOP1HI0UZRA3FSI+Zakhrc/Gy26YQsLI+m5V5DHxroHRJqCDLKJp7Hixn8zyaF7g==", "requires": { - "@types/node": "*" + "@types/node": "10.12.0" } }, "@types/js-yaml": { @@ -28,12 +56,867 @@ "integrity": "sha1-0DTh0ynkbo0Pc3yajbl/aPgbU4I=", "dev": true }, + "abab": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", + "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", + "dev": true + }, + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + }, + "acorn-globals": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz", + "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==", + "dev": true, + "requires": { + "acorn": "6.0.2", + "acorn-walk": "6.1.0" + }, + "dependencies": { + "acorn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.2.tgz", + "integrity": "sha512-GXmKIvbrN3TV7aVqAzVFaMW8F8wzVX7voEBRO3bDA64+EX37YSayggRJP5Xig6HYHBkWKpFg9W5gg6orklubhg==", + "dev": true + } + } + }, + "acorn-walk": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.0.tgz", + "integrity": "sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg==", + "dev": true + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "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==", + "dev": true, + "requires": { + "color-convert": "1.9.3" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "3.1.10", + "normalize-path": "2.1.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": 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.3", + "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", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": 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", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": 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", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": 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", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": 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", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": 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", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": 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.13", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + } + } + }, + "append-transform": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", + "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", + "dev": true, + "requires": { + "default-require-extensions": "1.0.0" + } + }, "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.2" + "sprintf-js": "1.0.3" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "2.1.2" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "dev": true, + "requires": { + "lodash": "4.17.11" + } + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "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=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": 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" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "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=", + "dev": true + } + } + }, + "babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-generator": "6.26.1", + "babel-helpers": "6.24.1", + "babel-messages": "6.23.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "convert-source-map": "1.6.0", + "debug": "2.6.9", + "json5": "0.5.1", + "lodash": "4.17.11", + "minimatch": "3.0.4", + "path-is-absolute": "1.0.1", + "private": "0.1.8", + "slash": "1.0.0", + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": 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.11", + "source-map": "0.5.7", + "trim-right": "1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-jest": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.6.0.tgz", + "integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==", + "dev": true, + "requires": { + "babel-plugin-istanbul": "4.1.6", + "babel-preset-jest": "23.2.0" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-istanbul": { + "version": "4.1.6", + "resolved": "http://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz", + "integrity": "sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "6.13.0", + "find-up": "2.1.0", + "istanbul-lib-instrument": "1.10.2", + "test-exclude": "4.2.3" + } + }, + "babel-plugin-jest-hoist": { + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz", + "integrity": "sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc=", + "dev": true + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", + "dev": true + }, + "babel-preset-jest": { + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz", + "integrity": "sha1-jsegOhOPABoaj7HoETZSvxpV2kY=", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "23.2.0", + "babel-plugin-syntax-object-rest-spread": "6.13.0" + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "dev": true, + "requires": { + "babel-core": "6.26.3", + "babel-runtime": "6.26.0", + "core-js": "2.5.7", + "home-or-tmp": "2.0.0", + "lodash": "4.17.11", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "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==", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + } + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "2.5.7", + "regenerator-runtime": "0.11.1" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.11" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": 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.11" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.11", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": 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", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.3" + } + }, + "browser-process-hrtime": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", + "dev": true + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "requires": { + "resolve": "1.1.7" + } + }, + "bser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", + "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", + "dev": true, + "requires": { + "node-int64": "0.4.0" } }, "buffer-from": { @@ -41,66 +924,4602 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true }, - "fs-extra": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.0.tgz", - "integrity": "sha512-EglNDLRpmaTWiD/qraZn6HREAEAHJcJOmxNEYwq6xeMKnVMAy3GUcFB+wXt2C6k4CNvB/mP1y/U3dzvKKj5OtQ==", + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "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", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } } }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true }, - "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "capture-exit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-1.2.0.tgz", + "integrity": "sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28=", + "dev": true, "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "rsvp": "3.6.2" } }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, "requires": { - "graceful-fs": "^4.1.6" + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.5.0" } }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "dev": true }, - "source-map-support": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", - "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "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", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } } }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "wrap-ansi": "2.1.0" + } }, - "universalify": { + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "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=", + "dev": true + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true, + "optional": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==", + "dev": true + }, + "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=", + "dev": true + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.3", + "shebang-command": "1.2.0", + "which": "1.3.1" + } + }, + "cssom": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.4.tgz", + "integrity": "sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog==", + "dev": true + }, + "cssstyle": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.1.1.tgz", + "integrity": "sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog==", + "dev": true, + "requires": { + "cssom": "0.3.4" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "requires": { + "abab": "2.0.0", + "whatwg-mimetype": "2.2.0", + "whatwg-url": "7.0.0" + }, + "dependencies": { + "whatwg-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", + "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "dev": true, + "requires": { + "lodash.sortby": "4.7.0", + "tr46": "1.0.1", + "webidl-conversions": "4.0.2" + } + } + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "default-require-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", + "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", + "dev": true, + "requires": { + "strip-bom": "2.0.0" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "4.0.2" + } + }, + "ecc-jsbn": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "0.1.1", + "safer-buffer": "2.1.2" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "es-abstract": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", + "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "dev": true, + "requires": { + "es-to-primitive": "1.2.0", + "function-bind": "1.1.1", + "has": "1.0.3", + "is-callable": "1.1.4", + "is-regex": "1.0.4" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "1.1.4", + "is-date-object": "1.0.1", + "is-symbol": "1.0.2" + } + }, + "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=", + "dev": true + }, + "escodegen": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz", + "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==", + "dev": true, + "requires": { + "esprima": "3.1.3", + "estraverse": "4.2.0", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.6.1" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "exec-sh": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz", + "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==", + "dev": true, + "requires": { + "merge": "1.2.1" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": 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" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.4" + } + }, + "expect": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-23.6.0.tgz", + "integrity": "sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "jest-diff": "23.6.0", + "jest-get-type": "22.4.3", + "jest-matcher-utils": "23.6.0", + "jest-message-util": "23.4.0", + "jest-regex-util": "23.3.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fb-watchman": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", + "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", + "dev": true, + "requires": { + "bser": "2.0.0" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", + "dev": true, + "requires": { + "glob": "7.1.3", + "minimatch": "3.0.4" + } + }, + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "3.1.1", + "repeat-element": "1.1.3", + "repeat-string": "1.6.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.7", + "mime-types": "2.1.21" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "0.2.2" + } + }, + "fs-extra": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.0.tgz", + "integrity": "sha512-EglNDLRpmaTWiD/qraZn6HREAEAHJcJOmxNEYwq6xeMKnVMAy3GUcFB+wXt2C6k4CNvB/mP1y/U3dzvKKj5OtQ==", + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.11.1", + "node-pre-gyp": "0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "optional": 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" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": "2.1.2" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.9", + "iconv-lite": "0.4.21", + "sax": "1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.2.0", + "nopt": "4.0.1", + "npm-packlist": "1.1.10", + "npmlog": "4.1.2", + "rc": "1.2.7", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "4.4.1" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.5.1", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "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.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.4", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true, + "dev": true + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": 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" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true + }, + "handlebars": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz", + "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==", + "dev": true, + "requires": { + "async": "2.6.1", + "optimist": "0.6.1", + "source-map": "0.6.1", + "uglify-js": "3.4.9" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", + "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "dev": true, + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "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=", + "dev": true + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "1.0.5" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.15.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": "2.1.2" + } + }, + "import-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", + "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", + "dev": true, + "requires": { + "pkg-dir": "2.0.0", + "resolve-cwd": "2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "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=", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "1.4.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "requires": { + "ci-info": "1.6.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": 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", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-generator-fn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-1.0.0.tgz", + "integrity": "sha1-lp1J4bszKfa7fwkIm+JleLLd1Go=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "1.0.3" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-api": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz", + "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", + "dev": true, + "requires": { + "async": "2.6.1", + "fileset": "2.0.3", + "istanbul-lib-coverage": "1.2.1", + "istanbul-lib-hook": "1.2.2", + "istanbul-lib-instrument": "1.10.2", + "istanbul-lib-report": "1.1.5", + "istanbul-lib-source-maps": "1.2.6", + "istanbul-reports": "1.5.1", + "js-yaml": "3.12.0", + "mkdirp": "0.5.1", + "once": "1.4.0" + } + }, + "istanbul-lib-coverage": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz", + "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", + "dev": true, + "requires": { + "append-transform": "0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", + "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", + "dev": 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.1", + "semver": "5.6.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz", + "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "1.2.1", + "mkdirp": "0.5.1", + "path-parse": "1.0.6", + "supports-color": "3.2.3" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz", + "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", + "dev": true, + "requires": { + "debug": "3.2.6", + "istanbul-lib-coverage": "1.2.1", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "source-map": "0.5.7" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz", + "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", + "dev": true, + "requires": { + "handlebars": "4.0.12" + } + }, + "jest": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-23.6.0.tgz", + "integrity": "sha512-lWzcd+HSiqeuxyhG+EnZds6iO3Y3ZEnMrfZq/OTGvF/C+Z4fPMCdhWTGSAiO2Oym9rbEXfwddHhh6jqrTF3+Lw==", + "dev": true, + "requires": { + "import-local": "1.0.0", + "jest-cli": "23.6.0" + }, + "dependencies": { + "jest-cli": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.6.0.tgz", + "integrity": "sha512-hgeD1zRUp1E1zsiyOXjEn4LzRLWdJBV//ukAHGlx6s5mfCNJTbhbHjgxnDUXA8fsKWN/HqFFF6X5XcCwC/IvYQ==", + "dev": true, + "requires": { + "ansi-escapes": "3.1.0", + "chalk": "2.4.1", + "exit": "0.1.2", + "glob": "7.1.3", + "graceful-fs": "4.1.11", + "import-local": "1.0.0", + "is-ci": "1.2.1", + "istanbul-api": "1.3.7", + "istanbul-lib-coverage": "1.2.1", + "istanbul-lib-instrument": "1.10.2", + "istanbul-lib-source-maps": "1.2.6", + "jest-changed-files": "23.4.2", + "jest-config": "23.6.0", + "jest-environment-jsdom": "23.4.0", + "jest-get-type": "22.4.3", + "jest-haste-map": "23.6.0", + "jest-message-util": "23.4.0", + "jest-regex-util": "23.3.0", + "jest-resolve-dependencies": "23.6.0", + "jest-runner": "23.6.0", + "jest-runtime": "23.6.0", + "jest-snapshot": "23.6.0", + "jest-util": "23.4.0", + "jest-validate": "23.6.0", + "jest-watcher": "23.4.0", + "jest-worker": "23.2.0", + "micromatch": "2.3.11", + "node-notifier": "5.3.0", + "prompts": "0.1.14", + "realpath-native": "1.0.2", + "rimraf": "2.6.2", + "slash": "1.0.0", + "string-length": "2.0.0", + "strip-ansi": "4.0.0", + "which": "1.3.1", + "yargs": "11.1.0" + } + }, + "yargs": { + "version": "11.1.0", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", + "dev": true, + "requires": { + "cliui": "4.1.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.3", + "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" + } + } + } + }, + "jest-changed-files": { + "version": "23.4.2", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-23.4.2.tgz", + "integrity": "sha512-EyNhTAUWEfwnK0Is/09LxoqNDOn7mU7S3EHskG52djOFS/z+IT0jT3h3Ql61+dklcG7bJJitIWEMB4Sp1piHmA==", + "dev": true, + "requires": { + "throat": "4.1.0" + } + }, + "jest-config": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-23.6.0.tgz", + "integrity": "sha512-i8V7z9BeDXab1+VNo78WM0AtWpBRXJLnkT+lyT+Slx/cbP5sZJ0+NDuLcmBE5hXAoK0aUp7vI+MOxR+R4d8SRQ==", + "dev": true, + "requires": { + "babel-core": "6.26.3", + "babel-jest": "23.6.0", + "chalk": "2.4.1", + "glob": "7.1.3", + "jest-environment-jsdom": "23.4.0", + "jest-environment-node": "23.4.0", + "jest-get-type": "22.4.3", + "jest-jasmine2": "23.6.0", + "jest-regex-util": "23.3.0", + "jest-resolve": "23.6.0", + "jest-util": "23.4.0", + "jest-validate": "23.6.0", + "micromatch": "2.3.11", + "pretty-format": "23.6.0" + } + }, + "jest-diff": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.6.0.tgz", + "integrity": "sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==", + "dev": true, + "requires": { + "chalk": "2.4.1", + "diff": "3.5.0", + "jest-get-type": "22.4.3", + "pretty-format": "23.6.0" + } + }, + "jest-docblock": { + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-23.2.0.tgz", + "integrity": "sha1-8IXh8YVI2Z/dabICB+b9VdkTg6c=", + "dev": true, + "requires": { + "detect-newline": "2.1.0" + } + }, + "jest-each": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-23.6.0.tgz", + "integrity": "sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg==", + "dev": true, + "requires": { + "chalk": "2.4.1", + "pretty-format": "23.6.0" + } + }, + "jest-environment-jsdom": { + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-23.4.0.tgz", + "integrity": "sha1-BWp5UrP+pROsYqFAosNox52eYCM=", + "dev": true, + "requires": { + "jest-mock": "23.2.0", + "jest-util": "23.4.0", + "jsdom": "11.12.0" + } + }, + "jest-environment-node": { + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-23.4.0.tgz", + "integrity": "sha1-V+gO0IQd6jAxZ8zozXlSHeuv3hA=", + "dev": true, + "requires": { + "jest-mock": "23.2.0", + "jest-util": "23.4.0" + } + }, + "jest-get-type": { + "version": "22.4.3", + "resolved": "http://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", + "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", + "dev": true + }, + "jest-haste-map": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-23.6.0.tgz", + "integrity": "sha512-uyNhMyl6dr6HaXGHp8VF7cK6KpC6G9z9LiMNsst+rJIZ8l7wY0tk8qwjPmEghczojZ2/ZhtEdIabZ0OQRJSGGg==", + "dev": true, + "requires": { + "fb-watchman": "2.0.0", + "graceful-fs": "4.1.11", + "invariant": "2.2.4", + "jest-docblock": "23.2.0", + "jest-serializer": "23.0.1", + "jest-worker": "23.2.0", + "micromatch": "2.3.11", + "sane": "2.5.2" + } + }, + "jest-jasmine2": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-23.6.0.tgz", + "integrity": "sha512-pe2Ytgs1nyCs8IvsEJRiRTPC0eVYd8L/dXJGU08GFuBwZ4sYH/lmFDdOL3ZmvJR8QKqV9MFuwlsAi/EWkFUbsQ==", + "dev": true, + "requires": { + "babel-traverse": "6.26.0", + "chalk": "2.4.1", + "co": "4.6.0", + "expect": "23.6.0", + "is-generator-fn": "1.0.0", + "jest-diff": "23.6.0", + "jest-each": "23.6.0", + "jest-matcher-utils": "23.6.0", + "jest-message-util": "23.4.0", + "jest-snapshot": "23.6.0", + "jest-util": "23.4.0", + "pretty-format": "23.6.0" + } + }, + "jest-leak-detector": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz", + "integrity": "sha512-f/8zA04rsl1Nzj10HIyEsXvYlMpMPcy0QkQilVZDFOaPbv2ur71X5u2+C4ZQJGyV/xvVXtCCZ3wQ99IgQxftCg==", + "dev": true, + "requires": { + "pretty-format": "23.6.0" + } + }, + "jest-matcher-utils": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz", + "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", + "dev": true, + "requires": { + "chalk": "2.4.1", + "jest-get-type": "22.4.3", + "pretty-format": "23.6.0" + } + }, + "jest-message-util": { + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.4.0.tgz", + "integrity": "sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8=", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0", + "chalk": "2.4.1", + "micromatch": "2.3.11", + "slash": "1.0.0", + "stack-utils": "1.0.1" + } + }, + "jest-mock": { + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-23.2.0.tgz", + "integrity": "sha1-rRxg8p6HGdR8JuETgJi20YsmETQ=", + "dev": true + }, + "jest-regex-util": { + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.3.0.tgz", + "integrity": "sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U=", + "dev": true + }, + "jest-resolve": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-23.6.0.tgz", + "integrity": "sha512-XyoRxNtO7YGpQDmtQCmZjum1MljDqUCob7XlZ6jy9gsMugHdN2hY4+Acz9Qvjz2mSsOnPSH7skBmDYCHXVZqkA==", + "dev": true, + "requires": { + "browser-resolve": "1.11.3", + "chalk": "2.4.1", + "realpath-native": "1.0.2" + } + }, + "jest-resolve-dependencies": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-23.6.0.tgz", + "integrity": "sha512-EkQWkFWjGKwRtRyIwRwI6rtPAEyPWlUC2MpzHissYnzJeHcyCn1Hc8j7Nn1xUVrS5C6W5+ZL37XTem4D4pLZdA==", + "dev": true, + "requires": { + "jest-regex-util": "23.3.0", + "jest-snapshot": "23.6.0" + } + }, + "jest-runner": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-23.6.0.tgz", + "integrity": "sha512-kw0+uj710dzSJKU6ygri851CObtCD9cN8aNkg8jWJf4ewFyEa6kwmiH/r/M1Ec5IL/6VFa0wnAk6w+gzUtjJzA==", + "dev": true, + "requires": { + "exit": "0.1.2", + "graceful-fs": "4.1.11", + "jest-config": "23.6.0", + "jest-docblock": "23.2.0", + "jest-haste-map": "23.6.0", + "jest-jasmine2": "23.6.0", + "jest-leak-detector": "23.6.0", + "jest-message-util": "23.4.0", + "jest-runtime": "23.6.0", + "jest-util": "23.4.0", + "jest-worker": "23.2.0", + "source-map-support": "0.5.9", + "throat": "4.1.0" + } + }, + "jest-runtime": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-23.6.0.tgz", + "integrity": "sha512-ycnLTNPT2Gv+TRhnAYAQ0B3SryEXhhRj1kA6hBPSeZaNQkJ7GbZsxOLUkwg6YmvWGdX3BB3PYKFLDQCAE1zNOw==", + "dev": true, + "requires": { + "babel-core": "6.26.3", + "babel-plugin-istanbul": "4.1.6", + "chalk": "2.4.1", + "convert-source-map": "1.6.0", + "exit": "0.1.2", + "fast-json-stable-stringify": "2.0.0", + "graceful-fs": "4.1.11", + "jest-config": "23.6.0", + "jest-haste-map": "23.6.0", + "jest-message-util": "23.4.0", + "jest-regex-util": "23.3.0", + "jest-resolve": "23.6.0", + "jest-snapshot": "23.6.0", + "jest-util": "23.4.0", + "jest-validate": "23.6.0", + "micromatch": "2.3.11", + "realpath-native": "1.0.2", + "slash": "1.0.0", + "strip-bom": "3.0.0", + "write-file-atomic": "2.3.0", + "yargs": "11.1.0" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "yargs": { + "version": "11.1.0", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", + "dev": true, + "requires": { + "cliui": "4.1.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.3", + "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" + } + } + } + }, + "jest-serializer": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-23.0.1.tgz", + "integrity": "sha1-o3dq6zEekP6D+rnlM+hRAr0WQWU=", + "dev": true + }, + "jest-snapshot": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-23.6.0.tgz", + "integrity": "sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg==", + "dev": true, + "requires": { + "babel-types": "6.26.0", + "chalk": "2.4.1", + "jest-diff": "23.6.0", + "jest-matcher-utils": "23.6.0", + "jest-message-util": "23.4.0", + "jest-resolve": "23.6.0", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "pretty-format": "23.6.0", + "semver": "5.6.0" + } + }, + "jest-util": { + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-23.4.0.tgz", + "integrity": "sha1-TQY8uSe68KI4Mf9hvsLLv0l5NWE=", + "dev": true, + "requires": { + "callsites": "2.0.0", + "chalk": "2.4.1", + "graceful-fs": "4.1.11", + "is-ci": "1.2.1", + "jest-message-util": "23.4.0", + "mkdirp": "0.5.1", + "slash": "1.0.0", + "source-map": "0.6.1" + } + }, + "jest-validate": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz", + "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", + "dev": true, + "requires": { + "chalk": "2.4.1", + "jest-get-type": "22.4.3", + "leven": "2.1.0", + "pretty-format": "23.6.0" + } + }, + "jest-watcher": { + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-23.4.0.tgz", + "integrity": "sha1-0uKM50+NrWxq/JIrksq+9u0FyRw=", + "dev": true, + "requires": { + "ansi-escapes": "3.1.0", + "chalk": "2.4.1", + "string-length": "2.0.0" + } + }, + "jest-worker": { + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-23.2.0.tgz", + "integrity": "sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk=", + "dev": true, + "requires": { + "merge-stream": "1.0.1" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "requires": { + "argparse": "1.0.10", + "esprima": "4.0.1" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "dev": true, + "requires": { + "abab": "2.0.0", + "acorn": "5.7.3", + "acorn-globals": "4.3.0", + "array-equal": "1.0.0", + "cssom": "0.3.4", + "cssstyle": "1.1.1", + "data-urls": "1.1.0", + "domexception": "1.0.1", + "escodegen": "1.11.0", + "html-encoding-sniffer": "1.0.2", + "left-pad": "1.3.0", + "nwsapi": "2.0.9", + "parse5": "4.0.0", + "pn": "1.1.0", + "request": "2.88.0", + "request-promise-native": "1.0.5", + "sax": "1.2.4", + "symbol-tree": "3.2.2", + "tough-cookie": "2.4.3", + "w3c-hr-time": "1.0.1", + "webidl-conversions": "4.0.2", + "whatwg-encoding": "1.0.5", + "whatwg-mimetype": "2.2.0", + "whatwg-url": "6.5.0", + "ws": "5.2.2", + "xml-name-validator": "3.0.0" + } + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "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=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "4.1.11" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "kleur": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-2.0.2.tgz", + "integrity": "sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ==", + "dev": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", + "dev": true + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": 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", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "3.0.2" + } + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.4" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "1.0.1" + } + }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "merge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", + "dev": true + }, + "merge-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "dev": true, + "requires": { + "readable-stream": "2.3.6" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", + "dev": true + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "dev": true, + "requires": { + "mime-db": "1.37.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nan": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", + "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": 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-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", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-notifier": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.3.0.tgz", + "integrity": "sha512-AhENzCSGZnZJgBARsUjnQ7DnZbzyP+HxlVXuD0xqAnvL8q+OqtSX7lGg9e8nHzwXkMMXNdVeqq4E2M3EUAqX6Q==", + "dev": true, + "requires": { + "growly": "1.3.0", + "semver": "5.6.0", + "shellwords": "0.1.1", + "which": "1.3.1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.7.1", + "is-builtin-module": "1.0.0", + "semver": "5.6.0", + "validate-npm-package-license": "3.0.4" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "nwsapi": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.9.tgz", + "integrity": "sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "object-keys": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "1.1.3", + "es-abstract": "1.12.0" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.8", + "wordwrap": "0.0.3" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "dev": true, + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.3.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.2" + } + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "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=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "2.1.0" + } + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-format": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", + "dev": true, + "requires": { + "ansi-regex": "3.0.0", + "ansi-styles": "3.2.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + } + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "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==", + "dev": true + }, + "prompts": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-0.1.14.tgz", + "integrity": "sha512-rxkyiE9YH6zAz/rZpywySLKkpaj0NMVyNw1qhsubdbjjSgcayjTShDreZGlFMcGSu5sab3bAKPfFk78PB90+8w==", + "dev": true, + "requires": { + "kleur": "2.0.2", + "sisteransi": "0.1.1" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "dev": true, + "requires": { + "is-number": "4.0.0", + "kind-of": "6.0.2", + "math-random": "1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": 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", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "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" + } + }, + "realpath-native": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.2.tgz", + "integrity": "sha512-+S3zTvVt9yTntFrBpm7TQmQ3tzpCrnA1a/y+3cUHAc9ZR6aIjG0WNLR+Rj79QpJktY+VeW/TQtFlQ1bzsehI8g==", + "dev": true, + "requires": { + "util.promisify": "1.0.0" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.8.0", + "caseless": "0.12.0", + "combined-stream": "1.0.7", + "extend": "3.0.2", + "forever-agent": "0.6.1", + "form-data": "2.3.3", + "har-validator": "5.1.0", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.21", + "oauth-sign": "0.9.0", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "tough-cookie": "2.4.3", + "tunnel-agent": "0.6.0", + "uuid": "3.3.2" + } + }, + "request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "dev": true, + "requires": { + "lodash": "4.17.11" + } + }, + "request-promise-native": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", + "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", + "dev": true, + "requires": { + "request-promise-core": "1.1.1", + "stealthy-require": "1.1.1", + "tough-cookie": "2.4.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.3" + } + }, + "rsvp": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", + "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==", + "dev": true + }, + "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==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "0.1.15" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sane": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/sane/-/sane-2.5.2.tgz", + "integrity": "sha1-tNwYYcIbQn6SlQej51HiosuKs/o=", + "dev": true, + "requires": { + "anymatch": "2.0.0", + "capture-exit": "1.2.0", + "exec-sh": "0.2.2", + "fb-watchman": "2.0.0", + "fsevents": "1.2.4", + "micromatch": "3.1.10", + "minimist": "1.2.0", + "walker": "1.0.7", + "watch": "0.18.0" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": 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.3", + "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", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": 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", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": 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", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": 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", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": 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", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": 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", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": 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.13", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": 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", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "sisteransi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz", + "integrity": "sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g==", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": 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.2", + "use": "3.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "2.1.2", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, + "source-map-support": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", + "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", + "requires": { + "buffer-from": "1.1.1", + "source-map": "0.6.1" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.2.tgz", + "integrity": "sha512-q9hedtzyXHr5S0A1vEPoK/7l8NpfkFYTq6iCY+Pno2ZbdZR6WexZFtqeVGkGxW3TEJMN914Z55EnAGMmenlIQQ==", + "dev": true, + "requires": { + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.1" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "2.2.0", + "spdx-license-ids": "3.0.1" + } + }, + "spdx-license-ids": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.1.tgz", + "integrity": "sha512-TfOfPcYGBB5sDuPn3deByxPhmfegAhpDYKSOXZQN81Oyrrif8ZCodOLzK3AesELnCx03kikhyDwh0pfvvQvF8w==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "3.0.2" + } + }, + "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.15.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.1.tgz", + "integrity": "sha512-mSdgNUaidk+dRU5MhYtN9zebdzF2iG0cNPWy8HG+W8y+fT1JnSkh0fzzpjOa0L7P8i1Rscz38t0h4gPcKz43xA==", + "dev": true, + "requires": { + "asn1": "0.2.4", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.2", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.2", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "safer-buffer": "2.1.2", + "tweetnacl": "0.14.5" + } + }, + "stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "string-length": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", + "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", + "dev": true, + "requires": { + "astral-regex": "1.0.0", + "strip-ansi": "4.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "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==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + } + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + }, + "symbol-tree": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", + "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", + "dev": true + }, + "test-exclude": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.3.tgz", + "integrity": "sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA==", + "dev": true, + "requires": { + "arrify": "1.0.1", + "micromatch": "2.3.11", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "require-main-filename": "1.0.1" + } + }, + "throat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", + "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", + "dev": true + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": 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", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + } + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "1.1.29", + "punycode": "1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "2.1.1" + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "uglify-js": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", + "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", + "dev": true, + "optional": true, + "requires": { + "commander": "2.17.1", + "source-map": "0.6.1" + } + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": 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", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "1.1.3", + "object.getownpropertydescriptors": "2.0.3" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "3.0.2", + "spdx-expression-parse": "3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "dev": true, + "requires": { + "browser-process-hrtime": "0.1.3" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.11" + } + }, + "watch": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", + "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", + "dev": true, + "requires": { + "exec-sh": "0.2.2", + "minimist": "1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz", + "integrity": "sha512-5YSO1nMd5D1hY3WzAQV3PzZL83W3YeyR1yW9PcH26Weh1t+Vzh9B6XkDh7aXm83HBZ4nSMvkjvN2H2ySWIvBgw==", + "dev": true + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "4.7.0", + "tr46": "1.0.1", + "webidl-conversions": "4.0.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "signal-exit": "3.0.2" + } + }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "dev": true, + "requires": { + "async-limiter": "1.0.0" + } + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs-parser": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", + "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "dev": true, + "requires": { + "camelcase": "4.1.0" + } } } } diff --git a/packages/@aws-cdk/applet-js/package.json b/packages/@aws-cdk/applet-js/package.json index 039e14afe1792..abb9adcc47967 100644 --- a/packages/@aws-cdk/applet-js/package.json +++ b/packages/@aws-cdk/applet-js/package.json @@ -11,7 +11,7 @@ "build": "cdk-build", "watch": "cdk-watch", "lint": "cdk-lint", - "test": "cdk-test", + "test": "cdk-test && /bin/bash test/test-applet.sh", "pkglint": "pkglint -f", "package": "cdk-package" }, diff --git a/packages/@aws-cdk/applet-js/test/expected1.json b/packages/@aws-cdk/applet-js/test/expected1.json index 375db10291244..5f49964fdcf1e 100644 --- a/packages/@aws-cdk/applet-js/test/expected1.json +++ b/packages/@aws-cdk/applet-js/test/expected1.json @@ -1,39 +1,35 @@ { - "stacks": [ - { - "name": "TestApplet", - "template": { - "Parameters": { - "P1": { - "Type": "String", - "Default": "hello" - }, - "P2": { - "Type": "Number", - "Default": 123 - } - } + "name": "TestApplet", + "template": { + "Parameters": { + "P1": { + "Type": "String", + "Default": "hello" }, - "metadata": { - "/TestApplet/P1": [ - { - "type": "aws:cdk:logicalId", - "data": "P1", - "trace": [ - "**REDACTED**" - ] - } - ], - "/TestApplet/P2": [ - { - "type": "aws:cdk:logicalId", - "data": "P2", - "trace": [ - "**REDACTED**" - ] - } - ] + "P2": { + "Type": "Number", + "Default": 123 } } - ] -} \ No newline at end of file + }, + "metadata": { + "/TestApplet/P1": [ + { + "type": "aws:cdk:logicalId", + "data": "P1", + "trace": [ + "**REDACTED**" + ] + } + ], + "/TestApplet/P2": [ + { + "type": "aws:cdk:logicalId", + "data": "P2", + "trace": [ + "**REDACTED**" + ] + } + ] + } +} diff --git a/packages/@aws-cdk/applet-js/test/expected2.json b/packages/@aws-cdk/applet-js/test/expected2.json index 34d1826651cff..3ef09cc69797f 100644 --- a/packages/@aws-cdk/applet-js/test/expected2.json +++ b/packages/@aws-cdk/applet-js/test/expected2.json @@ -1,52 +1,48 @@ { - "stacks": [ - { - "name": "TestApplet", - "template": { - "Parameters": { - "P1": { - "Type": "String", - "Default": "hello" - }, - "P2": { - "Type": "Number", - "Default": 123 - }, - "P3": { - "Type": "StringList", - "Default": "hello,this,is,awesome,12345" - } - } + "name": "TestApplet", + "template": { + "Parameters": { + "P1": { + "Type": "String", + "Default": "hello" }, - "metadata": { - "/TestApplet/P1": [ - { - "type": "aws:cdk:logicalId", - "data": "P1", - "trace": [ - "**REDACTED**" + "P2": { + "Type": "Number", + "Default": 123 + }, + "P3": { + "Type": "StringList", + "Default": "hello,this,is,awesome,12345" + } + } + }, + "metadata": { + "/TestApplet/P1": [ + { + "type": "aws:cdk:logicalId", + "data": "P1", + "trace": [ + "**REDACTED**" ] - } - ], - "/TestApplet/P2": [ - { - "type": "aws:cdk:logicalId", - "data": "P2", - "trace": [ - "**REDACTED**" + } + ], + "/TestApplet/P2": [ + { + "type": "aws:cdk:logicalId", + "data": "P2", + "trace": [ + "**REDACTED**" ] - } - ], - "/TestApplet/P3": [ - { - "type": "aws:cdk:logicalId", - "data": "P3", - "trace": [ - "**REDACTED**" + } + ], + "/TestApplet/P3": [ + { + "type": "aws:cdk:logicalId", + "data": "P3", + "trace": [ + "**REDACTED**" ] - } - ] } - } - ] -} \ No newline at end of file + ] + } +} diff --git a/packages/@aws-cdk/applet-js/test/expected3.json b/packages/@aws-cdk/applet-js/test/expected3.json index 5de08918673b3..495c9527bb181 100644 --- a/packages/@aws-cdk/applet-js/test/expected3.json +++ b/packages/@aws-cdk/applet-js/test/expected3.json @@ -1,11 +1,7 @@ { - "stacks": [ - { - "name": "Applet", - "template": { - "Description": "this should be reflected in the template description" - }, - "metadata": {} - } - ] -} \ No newline at end of file + "name": "Applet", + "template": { + "Description": "this should be reflected in the template description" + }, + "metadata": {} +} diff --git a/packages/@aws-cdk/applet-js/test/manual-test-npm.yaml b/packages/@aws-cdk/applet-js/test/manual-test-npm.yaml new file mode 100644 index 0000000000000..6d5e422492d40 --- /dev/null +++ b/packages/@aws-cdk/applet-js/test/manual-test-npm.yaml @@ -0,0 +1 @@ +applet: npm://@aws-cdk/aws-ecs:Hello diff --git a/packages/@aws-cdk/applet-js/test/strip-stacktrace.ts b/packages/@aws-cdk/applet-js/test/strip-stacktrace.ts deleted file mode 100644 index 154b2449bc897..0000000000000 --- a/packages/@aws-cdk/applet-js/test/strip-stacktrace.ts +++ /dev/null @@ -1,26 +0,0 @@ -import 'source-map-support/register'; - -function outputPatchedJson(text: string) { - const document: any = JSON.parse(text); - for (const stack of (document.stacks as any[])) { - for (const key of Object.keys(stack.metadata)) { - if (!stack.metadata[key]) { continue; } - for (const entry of (stack.metadata[key] as any[])) { - if (entry.trace) { entry.trace = ['**REDACTED**']; } - } - } - } - if ('runtime' in document) { - delete document.runtime; - } - process.stdout.write(JSON.stringify(document, null, 2)); -} - -function main() { - let inputText: string = ''; - process.stdin.setEncoding('utf8') - .on('data', chunk => inputText += chunk) - .on('end', () => outputPatchedJson(inputText)); -} - -main(); diff --git a/packages/@aws-cdk/applet-js/test/test-applet.ts b/packages/@aws-cdk/applet-js/test/test-applet.ts index 6d228770db998..685fbc98f1422 100644 --- a/packages/@aws-cdk/applet-js/test/test-applet.ts +++ b/packages/@aws-cdk/applet-js/test/test-applet.ts @@ -1,4 +1,4 @@ -import { App, Parameter, Stack, StackProps } from '@aws-cdk/cdk'; +import { App, Construct, Parameter, Stack, StackProps } from '@aws-cdk/cdk'; export interface TestAppletProps extends StackProps { prop1: string @@ -30,3 +30,15 @@ export class Applet extends Stack { this.templateOptions.description = props.desc; } } + +interface NoStackAppletProps { + argument: string; +} + +export class NoStackApplet extends Construct { + constructor(parent: Construct, id: string, props: NoStackAppletProps) { + super(parent, id); + + new Parameter(this, 'P1', { default: props.argument, type: 'String' }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/applet-js/test/test-fromnpm.yaml b/packages/@aws-cdk/applet-js/test/test-fromnpm.yaml new file mode 100644 index 0000000000000..5ddc286f66755 --- /dev/null +++ b/packages/@aws-cdk/applet-js/test/test-fromnpm.yaml @@ -0,0 +1 @@ +applet: npm:// diff --git a/packages/@aws-cdk/applet-js/test/test-multistack-expected.json b/packages/@aws-cdk/applet-js/test/test-multistack-expected.json new file mode 100644 index 0000000000000..d3cfb592f5396 --- /dev/null +++ b/packages/@aws-cdk/applet-js/test/test-multistack-expected.json @@ -0,0 +1,35 @@ +{ + "name": "Stack2", + "template": { + "Parameters": { + "P1": { + "Type": "String", + "Default": "stack2" + }, + "P2": { + "Type": "Number", + "Default": 456 + } + } + }, + "metadata": { + "/Stack2/P1": [ + { + "type": "aws:cdk:logicalId", + "data": "P1", + "trace": [ + "**REDACTED**" + ] + } + ], + "/Stack2/P2": [ + { + "type": "aws:cdk:logicalId", + "data": "P2", + "trace": [ + "**REDACTED**" + ] + } + ] + } +} diff --git a/packages/@aws-cdk/applet-js/test/test-multistack.yaml b/packages/@aws-cdk/applet-js/test/test-multistack.yaml new file mode 100644 index 0000000000000..e2aec470d1a3a --- /dev/null +++ b/packages/@aws-cdk/applet-js/test/test-multistack.yaml @@ -0,0 +1,9 @@ +--- +- applet: ./test-applet:TestApplet + name: Stack1 + prop1: stack1 + prop2: 123 +- applet: ./test-applet:TestApplet + name: Stack2 + prop1: stack2 + prop2: 456 diff --git a/packages/@aws-cdk/applet-js/test/test-nonstack-expected.json b/packages/@aws-cdk/applet-js/test/test-nonstack-expected.json new file mode 100644 index 0000000000000..3abdc38dc8187 --- /dev/null +++ b/packages/@aws-cdk/applet-js/test/test-nonstack-expected.json @@ -0,0 +1,22 @@ +{ + "name": "NoStackApplet", + "template": { + "Parameters": { + "P1": { + "Type": "String", + "Default": "this should be reflected in the template description" + } + } + }, + "metadata": { + "/NoStackApplet/Default/P1": [ + { + "type": "aws:cdk:logicalId", + "data": "P1", + "trace": [ + "**REDACTED**" + ] + } + ] + } +} diff --git a/packages/@aws-cdk/applet-js/test/test-nonstack.yaml b/packages/@aws-cdk/applet-js/test/test-nonstack.yaml new file mode 100644 index 0000000000000..c9dbcdd7f6170 --- /dev/null +++ b/packages/@aws-cdk/applet-js/test/test-nonstack.yaml @@ -0,0 +1,4 @@ +#!/usr/bin/env cdk-applet-js +applet: ./test-applet:NoStackApplet +argument: this should be reflected in the template description + diff --git a/packages/@aws-cdk/applet-js/test/test.applets.ts b/packages/@aws-cdk/applet-js/test/test.applets.ts new file mode 100644 index 0000000000000..a461eacfd8509 --- /dev/null +++ b/packages/@aws-cdk/applet-js/test/test.applets.ts @@ -0,0 +1,172 @@ +import child_process = require('child_process'); +import fs = require('fs'); +import { Test } from 'nodeunit'; +import os = require('os'); +import path = require('path'); + +export = { + 'basic test 1'(test: Test) { + expectMatch(test, 'test/expected1.json', getStack('TestApplet', synthesizeApplet('test/test1.yaml'))); + test.done(); + }, + + 'basic test 2'(test: Test) { + expectMatch(test, 'test/expected2.json', getStack('TestApplet', synthesizeApplet('test/test2.yaml'))); + test.done(); + }, + + 'can use shebang'(test: Test) { + fs.chmodSync('test/test3.yaml', 0o755); + expectMatch(test, 'test/expected3.json', getStack('Applet', synthesizeApplet('test/test3.yaml', true))); + test.done(); + }, + + 'test non stack construct'(test: Test) { + expectMatch(test, 'test/test-nonstack-expected.json', getStack('NoStackApplet', synthesizeApplet('test/test-nonstack.yaml'))); + test.done(); + }, + + 'test multiple stacks'(test: Test) { + expectMatch(test, 'test/test-multistack-expected.json', getStack('Stack2', synthesizeApplet('test/test-multistack.yaml'))); + test.done(); + }, + + 'expect failure 4'(test: Test) { + test.throws(() => { + synthesizeApplet('test/negative-test4.yaml'); + }, /but it must be in the form/); + test.done(); + }, + + 'expect failure 5'(test: Test) { + test.throws(() => { + synthesizeApplet('test/negative-test5.yaml'); + }, /Cannot find module/); + test.done(); + }, + + 'expect failure 6'(test: Test) { + test.throws(() => { + synthesizeApplet('test/negative-test6.yaml'); + }, /Cannot find applet class/); + test.done(); + }, + + 'expect failure 7'(test: Test) { + test.throws(() => { + synthesizeApplet('test/negative-test7.yaml'); + }, /but it must be in the form/); + test.done(); + }, +}; + +function expectMatch(test: Test, expectedFile: string, stack: any) { + try { + const expected = JSON.parse(fs.readFileSync(expectedFile, { encoding: 'utf-8' })); + test.deepEqual(stack, expected); + } catch (e) { + if (e.code === 'ENOENT') { + // tslint:disable-next-line:no-console + console.log(JSON.stringify(stack, undefined, 2)); + throw new Error(`Make a file ${expectedFile} with the previous contents`); + } + } +} + +function synthesizeApplet(yamlFile: string, direct = false) { + // Can't depend on aws-cdk here, so we're reimplementing cx-api. + // tslint:disable-next-line:no-console + console.log('Writing to ', os.tmpdir()); + + const command = direct ? yamlFile : 'cdk-applet-js'; + const args = direct ? [] : [yamlFile]; + + child_process.execFileSync(command, args, { + env: { + ...process.env, + CDK_OUTDIR: os.tmpdir(), + PATH: 'bin:' + process.env.PATH + } + }); + + return JSON.parse(fs.readFileSync(path.join(os.tmpdir(), 'cdk.out'), { encoding: 'utf-8' })); +} + +function getStack(stackName: string, allStacks: any) { + for (const stack of allStacks.stacks) { + if (stack.name === stackName) { + return stripStackMetadata(stack); + } + } + + // tslint:disable-next-line:no-console + console.log(allStacks); + throw new Error('Could not find stack: ' + stackName); +} + +function stripStackMetadata(stack: any) { + for (const key of Object.keys(stack.metadata)) { + if (!stack.metadata[key]) { continue; } + for (const entry of (stack.metadata[key] as any[])) { + if (entry.trace) { entry.trace = ['**REDACTED**']; } + } + } + delete stack.environment; + return stack; +} + +// cdk-applet-js test1.yaml '{ "type": "synth", "stacks": ["TestApplet"] }' | node strip-stacktrace.js > /tmp/actual1.json +// expect_success diff expected1.json /tmp/actual1.json + +// #!/bin/bash +// set -euo pipefail +// cd $(dirname $0) +// export PATH=../bin:$PATH + +// announce() { +// echo "-------------------------------------------------" +// echo "$@" +// } + +// expect_success() { +// announce $@ +// set +e +// $@ +// local exit_code=$? +// set -e + +// if [ "${exit_code}" -ne 0 ]; then +// echo "Command expected to succeed: $@" +// exit 1 +// fi +// } + +// expect_failure() { +// announce $@ +// set +e +// $@ +// local exit_code=$? +// set -e +// if [ "${exit_code}" -eq 0 ]; then +// echo "Command expected to fail: $@" +// exit 1 +// fi +// } + +// cdk-applet-js test1.yaml '{ "type": "synth", "stacks": ["TestApplet"] }' | node strip-stacktrace.js > /tmp/actual1.json +// expect_success diff expected1.json /tmp/actual1.json + +// cdk-applet-js test2.yaml '{ "type": "synth", "stacks": ["TestApplet"] }' | node strip-stacktrace.js > /tmp/actual2.json +// expect_success diff expected2.json /tmp/actual2.json + +// # applets can use the host as shebang +// chmod +x ./test3.yaml # since codebuild loses permissions +// ./test3.yaml '{ "type": "synth", "stacks": ["Applet"] }' | node strip-stacktrace.js > /tmp/actual3.json +// expect_success diff expected3.json /tmp/actual3.json + +// expect_failure cdk-applet-js negative-test4.yaml +// expect_failure cdk-applet-js negative-test5.yaml +// expect_failure cdk-applet-js negative-test6.yaml +// expect_failure cdk-applet-js negative-test7.yaml + +// echo "PASS" \ No newline at end of file diff --git a/packages/@aws-cdk/applet-js/test/test.helpers.ts b/packages/@aws-cdk/applet-js/test/test.helpers.ts new file mode 100644 index 0000000000000..31d03ce711c46 --- /dev/null +++ b/packages/@aws-cdk/applet-js/test/test.helpers.ts @@ -0,0 +1,56 @@ +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import { extractModuleName, isStackConstructor, parseApplet } from '../lib/applet-helpers'; + +export = { + 'test that refactoring Stack didn\'t break Stack detection'(test: Test) { + test.equals(true, isStackConstructor(cdk.Stack)); + test.done(); + }, + + 'test package name extraction'(test: Test) { + test.equals('my-package', extractModuleName('my-package')); + test.equals('my-package', extractModuleName('my-package@1.0')); + test.equals('@scope/my-package', extractModuleName('@scope/my-package')); + test.equals('@scope/my-package', extractModuleName('@scope/my-package@1.0')); + test.done(); + }, + + 'test applet name extraction'(test: Test) { + test.deepEqual(parseApplet('applet'), { + moduleName: 'applet', + className: 'Applet' + }); + + test.deepEqual(parseApplet('applet:Class'), { + moduleName: 'applet', + className: 'Class' + }); + + test.deepEqual(parseApplet('npm://applet:Class'), { + npmPackage: 'applet', + moduleName: 'applet', + className: 'Class' + }); + + test.deepEqual(parseApplet('npm://applet@1.0:Class'), { + npmPackage: 'applet@1.0', + moduleName: 'applet', + className: 'Class' + }); + + test.deepEqual(parseApplet('npm://applet@1.0'), { + npmPackage: 'applet@1.0', + moduleName: 'applet', + className: 'Applet' + }); + + test.deepEqual(parseApplet('npm://@scope/applet@1.0'), { + npmPackage: '@scope/applet@1.0', + moduleName: '@scope/applet', + className: 'Applet' + }); + + test.done(); + } +}; \ No newline at end of file diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 256a5d3bc6b61..d5a18f1587adb 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -2,14 +2,10 @@ import 'source-map-support/register'; import cxapi = require('@aws-cdk/cx-api'); -import childProcess = require('child_process'); import colors = require('colors/safe'); import fs = require('fs-extra'); import YAML = require('js-yaml'); import minimatch = require('minimatch'); -import os = require('os'); -import path = require('path'); -import semver = require('semver'); import util = require('util'); import yargs = require('yargs'); import cdkUtil = require('../lib/util'); @@ -17,12 +13,13 @@ import cdkUtil = require('../lib/util'); import { bootstrapEnvironment, deployStack, destroyStack, loadToolkitInfo, Mode, SDK } from '../lib'; import contextplugins = require('../lib/contextplugins'); import { printStackDiff } from '../lib/diff'; +import { execProgram } from '../lib/exec'; import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init'; import { interactive } from '../lib/interactive'; import { data, debug, error, highlight, print, setVerbose, success, warning } from '../lib/logging'; import { PluginHost } from '../lib/plugin'; import { parseRenames } from '../lib/renames'; -import { Settings } from '../lib/settings'; +import { DEFAULTS, PER_USER_DEFAULTS, Settings } from '../lib/settings'; import { VERSION } from '../lib/version'; // tslint:disable-next-line:no-var-requires @@ -30,9 +27,6 @@ const promptly = require('promptly'); const DEFAULT_TOOLKIT_STACK_NAME = 'CDKToolkit'; -const DEFAULTS = 'cdk.json'; -const PER_USER_DEFAULTS = '~/.cdk.json'; - /** * Since app execution basically always synthesizes all the stacks, * we can invoke it once and cache the response for subsequent calls. @@ -384,7 +378,7 @@ async function initCommandLine() { // We may need to run the cloud executable multiple times in order to satisfy all missing context while (true) { - const response: cxapi.SynthesizeResponse = await execProgram(); + const response: cxapi.SynthesizeResponse = await execProgram(aws, config); const allMissing = cdkUtil.deepMerge(...response.stacks.map(s => s.missing)); if (!cdkUtil.isEmpty(allMissing)) { @@ -441,105 +435,6 @@ async function initCommandLine() { } } - /** Invokes the cloud executable and returns JSON output */ - async function execProgram(): Promise { - const env: { [key: string]: string } = { }; - - const context = config.get(['context']); - await populateDefaultEnvironmentIfNeeded(context); - - env[cxapi.CONTEXT_ENV] = JSON.stringify(context); - - const app = config.get(['app']); - if (!app) { - throw new Error(`--app is required either in command-line, in ${DEFAULTS} or in ${PER_USER_DEFAULTS}`); - } - - const commandLine = appToArray(app); - - const outdir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk')); - debug('outdir:', outdir); - env[cxapi.OUTDIR_ENV] = outdir; - - try { - const outfile = await exec(); - debug('outfile:', outfile); - if (!(await fs.pathExists(outfile))) { - throw new Error(`Unable to find output file ${outfile}`); - } - - const response = await fs.readJson(outfile); - debug(response); - return versionCheckResponse(response); - } finally { - debug('Removing outdir', outdir); - await fs.remove(outdir); - } - - async function exec() { - return new Promise((ok, fail) => { - // We use a slightly lower-level interface to: - // - // - Pass arguments in an array instead of a string, to get around a - // number of quoting issues introduced by the intermediate shell layer - // (which would be different between Linux and Windows). - // - // - Inherit stderr from controlling terminal. We don't use the captured value - // anway, and if the subprocess is printing to it for debugging purposes the - // user gets to see it sooner. Plus, capturing doesn't interact nicely with some - // processes like Maven. - const proc = childProcess.spawn(commandLine[0], commandLine.slice(1), { - stdio: ['ignore', 'inherit', 'inherit'], - detached: false, - env: { - ...process.env, - ...env - } - }); - - proc.on('error', fail); - - proc.on('exit', code => { - if (code === 0) { - return ok(path.join(outdir, cxapi.OUTFILE_NAME)); - } else { - return fail(new Error('Subprocess exited with error ' + code.toString())); - } - }); - }); - } - } - } - - /** - * Look at the type of response we get and upgrade it to the latest expected version - */ - function versionCheckResponse(response: cxapi.SynthesizeResponse): cxapi.SynthesizeResponse { - if (!response.version) { - // tslint:disable-next-line:max-line-length - throw new Error(`CDK Framework >= ${cxapi.PROTO_RESPONSE_VERSION} is required in order to interact with this version of the Toolkit.`); - } - - const frameworkVersion = semver.coerce(response.version); - const toolkitVersion = semver.coerce(cxapi.PROTO_RESPONSE_VERSION); - - // Should not happen, but I don't trust this library 100% either, so let's check for it to be safe - if (!frameworkVersion || !toolkitVersion) { throw new Error('SemVer library could not parse versions'); } - - if (semver.gt(frameworkVersion, toolkitVersion)) { - throw new Error(`CDK Toolkit >= ${response.version} is required in order to interact with this program.`); - } - - if (semver.lt(frameworkVersion, toolkitVersion)) { - // Toolkit protocol is newer than the framework version, and we KNOW the - // version. This is a scenario in which we could potentially do some - // upgrading of the response in the future. - // - // For now though, we simply reject old responses. - throw new Error(`CDK Framework >= ${cxapi.PROTO_RESPONSE_VERSION} is required in order to interact with this version of the Toolkit.`); - } - - return response; } /** @@ -767,39 +662,6 @@ async function initCommandLine() { }); } - /** - * Make sure the 'app' is an array - * - * If it's a string, split on spaces as a trivial way of tokenizing the command line. - */ - function appToArray(app: any) { - return typeof app === 'string' ? app.split(' ') : app; - } - - /** - * If we don't have region/account defined in context, we fall back to the default SDK behavior - * where region is retreived from ~/.aws/config and account is based on default credentials provider - * chain and then STS is queried. - * - * This is done opportunistically: for example, if we can't acccess STS for some reason or the region - * is not configured, the context value will be 'null' and there could failures down the line. In - * some cases, synthesis does not require region/account information at all, so that might be perfectly - * fine in certain scenarios. - * - * @param context The context key/value bash. - */ - async function populateDefaultEnvironmentIfNeeded(context: any) { - if (!(cxapi.DEFAULT_REGION_CONTEXT_KEY in context)) { - context[cxapi.DEFAULT_REGION_CONTEXT_KEY] = await aws.defaultRegion(); - debug(`Setting "${cxapi.DEFAULT_REGION_CONTEXT_KEY}" context to`, context[cxapi.DEFAULT_REGION_CONTEXT_KEY]); - } - - if (!(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY in context)) { - context[cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY] = await aws.defaultAccount(); - debug(`Setting "${cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY}" context to`, context[cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY]); - } - } - /** * Combine the names of a set of stacks using a comma */ diff --git a/packages/aws-cdk/lib/exec.ts b/packages/aws-cdk/lib/exec.ts new file mode 100644 index 0000000000000..50bc2007bf438 --- /dev/null +++ b/packages/aws-cdk/lib/exec.ts @@ -0,0 +1,154 @@ +import cxapi = require('@aws-cdk/cx-api'); +import childProcess = require('child_process'); +import fs = require('fs-extra'); +import os = require('os'); +import path = require('path'); +import semver = require('semver'); +import { DEFAULTS, PER_USER_DEFAULTS, Settings } from '../lib/settings'; +import { SDK } from './api'; +import { debug } from './logging'; + +/** Invokes the cloud executable and returns JSON output */ +export async function execProgram(aws: SDK, config: Settings): Promise { + const env: { [key: string]: string } = { }; + + const context = config.get(['context']); + await populateDefaultEnvironmentIfNeeded(aws, context); + + env[cxapi.CONTEXT_ENV] = JSON.stringify(context); + + const app = config.get(['app']); + if (!app) { + throw new Error(`--app is required either in command-line, in ${DEFAULTS} or in ${PER_USER_DEFAULTS}`); + } + + const commandLine = await guessExecutable(appToArray(app)); + + const outdir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk')); + debug('outdir:', outdir); + env[cxapi.OUTDIR_ENV] = outdir; + + try { + const outfile = await exec(); + debug('outfile:', outfile); + if (!(await fs.pathExists(outfile))) { + throw new Error(`Unable to find output file ${outfile}`); + } + + const response = await fs.readJson(outfile); + debug(response); + return versionCheckResponse(response); + } finally { + debug('Removing outdir', outdir); + await fs.remove(outdir); + } + + async function exec() { + return new Promise((ok, fail) => { + // We use a slightly lower-level interface to: + // + // - Pass arguments in an array instead of a string, to get around a + // number of quoting issues introduced by the intermediate shell layer + // (which would be different between Linux and Windows). + // + // - Inherit stderr from controlling terminal. We don't use the captured value + // anway, and if the subprocess is printing to it for debugging purposes the + // user gets to see it sooner. Plus, capturing doesn't interact nicely with some + // processes like Maven. + const proc = childProcess.spawn(commandLine[0], commandLine.slice(1), { + stdio: ['ignore', 'inherit', 'inherit'], + detached: false, + env: { + ...process.env, + ...env + } + }); + + proc.on('error', fail); + + proc.on('exit', code => { + if (code === 0) { + return ok(path.join(outdir, cxapi.OUTFILE_NAME)); + } else { + return fail(new Error('Subprocess exited with error ' + code.toString())); + } + }); + }); + } +} + +/** + * Look at the type of response we get and upgrade it to the latest expected version + */ +function versionCheckResponse(response: cxapi.SynthesizeResponse): cxapi.SynthesizeResponse { + if (!response.version) { + // tslint:disable-next-line:max-line-length + throw new Error(`CDK Framework >= ${cxapi.PROTO_RESPONSE_VERSION} is required in order to interact with this version of the Toolkit.`); + } + + const frameworkVersion = semver.coerce(response.version); + const toolkitVersion = semver.coerce(cxapi.PROTO_RESPONSE_VERSION); + + // Should not happen, but I don't trust this library 100% either, so let's check for it to be safe + if (!frameworkVersion || !toolkitVersion) { throw new Error('SemVer library could not parse versions'); } + + if (semver.gt(frameworkVersion, toolkitVersion)) { + throw new Error(`CDK Toolkit >= ${response.version} is required in order to interact with this program.`); + } + + if (semver.lt(frameworkVersion, toolkitVersion)) { + // Toolkit protocol is newer than the framework version, and we KNOW the + // version. This is a scenario in which we could potentially do some + // upgrading of the response in the future. + // + // For now though, we simply reject old responses. + throw new Error(`CDK Framework >= ${cxapi.PROTO_RESPONSE_VERSION} is required in order to interact with this version of the Toolkit.`); + } + + return response; +} + +/** + * If we don't have region/account defined in context, we fall back to the default SDK behavior + * where region is retreived from ~/.aws/config and account is based on default credentials provider + * chain and then STS is queried. + * + * This is done opportunistically: for example, if we can't acccess STS for some reason or the region + * is not configured, the context value will be 'null' and there could failures down the line. In + * some cases, synthesis does not require region/account information at all, so that might be perfectly + * fine in certain scenarios. + * + * @param context The context key/value bash. + */ +async function populateDefaultEnvironmentIfNeeded(aws: SDK, context: any) { + if (!(cxapi.DEFAULT_REGION_CONTEXT_KEY in context)) { + context[cxapi.DEFAULT_REGION_CONTEXT_KEY] = await aws.defaultRegion(); + debug(`Setting "${cxapi.DEFAULT_REGION_CONTEXT_KEY}" context to`, context[cxapi.DEFAULT_REGION_CONTEXT_KEY]); + } + + if (!(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY in context)) { + context[cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY] = await aws.defaultAccount(); + debug(`Setting "${cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY}" context to`, context[cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY]); + } +} + +/** + * Make sure the 'app' is an array + * + * If it's a string, split on spaces as a trivial way of tokenizing the command line. + */ +function appToArray(app: any) { + return typeof app === 'string' ? app.split(' ') : app; +} + +/** + * Guess the executable from the command-line argument + */ +async function guessExecutable(commandLine: string[]) { + if (commandLine.length === 1 && ['.yml', '.yaml'].includes(path.extname(commandLine[0]))) { + // Direct execution of a YAML file, assume that we're deploying an Applet + const appletBinary = path.resolve(require.resolve('@aws-cdk/applet-js')); + return [process.execPath, appletBinary, commandLine[0]]; + } + return commandLine; +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/settings.ts b/packages/aws-cdk/lib/settings.ts index 4c6a63ae39309..a6200cbfee399 100644 --- a/packages/aws-cdk/lib/settings.ts +++ b/packages/aws-cdk/lib/settings.ts @@ -6,6 +6,9 @@ import util = require('./util'); export type SettingsMap = {[key: string]: any}; +export const DEFAULTS = 'cdk.json'; +export const PER_USER_DEFAULTS = '~/.cdk.json'; + export class Settings { public static mergeAll(...settings: Settings[]): Settings { let ret = new Settings(); diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 29c3540d016e8..073e965c4c4bc 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -47,6 +47,7 @@ "dependencies": { "@aws-cdk/cloudformation-diff": "^0.14.1", "@aws-cdk/cx-api": "^0.14.1", + "@aws-cdk/applet-js": "^0.14.1", "archiver": "^2.1.1", "aws-sdk": "^2.259.1", "camelcase": "^5.0.0", From 2fd2e7c33d3d2ffb1b4b2c700c1e43863f4fa46f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 30 Oct 2018 16:01:15 +0100 Subject: [PATCH 100/140] Add docs and readme and a couple of tests --- packages/@aws-cdk/aws-ecs/README.md | 144 +++++++++++++++++- .../@aws-cdk/aws-ecs/lib/base/base-cluster.ts | 18 ++- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 47 +++++- .../aws-ecs/lib/base/base-task-definition.ts | 68 ++++++++- .../aws-ecs/lib/container-definition.ts | 103 ++++++++++++- .../@aws-cdk/aws-ecs/lib/container-image.ts | 19 +++ .../@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts | 89 ++++++++--- .../@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts | 20 ++- .../aws-ecs/lib/ecs/ecs-task-definition.ts | 40 ++++- .../aws-ecs/lib/fargate/fargate-cluster.ts | 52 ++++++- .../aws-ecs/lib/fargate/fargate-service.ts | 13 ++ .../lib/fargate/fargate-task-definition.ts | 9 ++ .../@aws-cdk/aws-ecs/lib/linux-parameters.ts | 91 ++++++++++- .../aws-ecs/lib/load-balanced-ecs-service.ts | 16 +- .../load-balanced-fargate-service-applet.ts | 10 ++ .../lib/load-balanced-fargate-service.ts | 12 ++ packages/@aws-cdk/aws-ecs/package.json | 1 + .../aws-ecs/test/ecs/test.ecs-service.ts | 33 ++++ .../aws-ecs/test/test.container-definition.ts | 31 ++++ packages/@aws-cdk/aws-ecs/test/test.ecs.ts | 8 - packages/@aws-cdk/aws-ecs/test/test.l3s.ts | 43 ++++++ tools/cdk-integ-tools/lib/integ-helpers.ts | 26 +++- 22 files changed, 828 insertions(+), 65 deletions(-) delete mode 100644 packages/@aws-cdk/aws-ecs/test/test.ecs.ts create mode 100644 packages/@aws-cdk/aws-ecs/test/test.l3s.ts diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 0b281e1184740..9943a85cedd00 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -1,2 +1,142 @@ -## The CDK Construct Library for AWS Elastic Container Service (ECS) -This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project. +## AWS Elastic Container Service (ECS) Construct Library + +This package contains constructs for working with **AWS Elastic Container +Service** (ECS). The simplest example of using this library looks like this: + +```ts +// Create an ECS cluster (backed by an AutoScaling group) +const cluster = new ecs.EcsCluster(this, 'Cluster', { + vpc, + size: 3, + instanceType: new InstanceType("t2.xlarge") +}); + +// Instantiate ECS Service with an automatic load balancer +const ecsService = new ecs.LoadBalancedEcsService(this, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), +}); +``` + +### Fargate vs ECS + +There are two sets of constructs in this library; one to run tasks on ECS and +one to run Tasks on fargate. + +- Use the `EcsCluster`, `EcsTaskDefinition` and `EcsService` constructs to + run tasks on EC2 instances running in your account. +- Use the `FargateCluster`, `FargateTaskDefinition` and `FargateService` + constructs to run tasks on instances that are managed for you by AWS. + +## Cluster + +An `EcsCluster` or `FargateCluster` defines a set of instances to run your +tasks on. If you create an ECS cluster, an AutoScalingGroup of EC2 instances +running the right AMI will implicitly be created for you. + +You can run many tasks on a single cluster. + +To create a cluster, go: + +```ts +const cluster = new ecs.FargateCluster(this, 'Cluster', { + vpc: vpc +}); +``` + +## TaskDefinition + +A `TaskDefinition` describes what a single copy of a **Task** should look like. +A task definition has one or more containers; typically, it has one +main container (the *default container* is the first one that's added +to the task definition, and it will be marked *essential*) and optionally +some supporting containers which are used to support the main container, +doings things like upload logs or metrics to monitoring services. + +To add containers to a `TaskDefinition`, call `addContainer()`: + +```ts +const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { + memoryMiB: '512' + cpu: 256 +}); + +taskDefinition.addContainer('main', { + // Use an image from DockerHub + image: ecs.DockerHub.image('amazon/amazon-ecs-sample') +}); +``` + +### Images + +Images supply the software that runs inside the container. Images can be +obtained from either DockerHub or from ECR repositories: + +* `ecs.DockerHub.image(imageName)`: use an publicly available image from + DockerHub. +* `repository.getImage(tag)`: use the given ECR repository as the image + to start. + +## Service + +A `Service` instantiates a `TaskDefinition` on a `Cluster` a given number of +times, optionally associating them with a load balnacer. Tasks that fail will +automatically be restarted. + +```ts +const taskDefinition; + +const service = new ecs.EcsService(this, 'Service', { + cluster, + taskDefinition, + desiredCount: 5 +}); +``` + +### Include a load balancer + +`Services` are load balancing targets and can be directly attached to load +balancers: + +```ts +const service = new ecs.FargateService(this, 'Service', { /* ... */ }); + +const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { vpc, internetFacing: true }); +const listener = lb.addListener('Listener', { port: 80 }); +listener.addTargets('ECS', { + port: 80, + targets: [service] +}); +``` + +There are two higher-level constructs available which include a load balancer for you: + +* `LoadBalancedFargateService` +* `LoadBalancedEcsService` + +## Task AutoScaling + +You can configure the task count of a service to match demand. Task AutoScaling is +configured by calling `autoScaleTaskCount()`: + +```ts +const scaling = service.autoScaleTaskCount({ maxCapacity: 10 }); +scaling.scaleOnCpuUtilization('CpuScaling', { + targetUtilizationPercent: 50 +}); +``` + +Task AutoScaling is powered by *Application AutoScaling*. Refer to that for +more information. + +## Instance AutoScaling + +If you're running on Fargate, AWS will manage the physical machines that your +containers are running on for you. If you're running an ECS cluster however, +your EC2 instances might fill up as your number of Tasks goes up. + +To avoid placement errors, you will want to configure AutoScaling for your +EC2 instance group so that your instance count scales with demand. + +TO BE IMPLEMENTED \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts index fe2d923994951..823ed7ceae715 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts @@ -3,6 +3,9 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { cloudformation } from '../ecs.generated'; +/** + * Basic cluster properties + */ export interface BaseClusterProps { /** * A name for the cluster. @@ -12,19 +15,28 @@ export interface BaseClusterProps { clusterName?: string; /** - * The VPC where your ECS instances will be running + * The VPC where your ECS instances will be running or your ENIs will be deployed */ vpc: ec2.VpcNetworkRef; } -export class BaseCluster extends cdk.Construct { +/** + * Base class for Ecs and Fargate clusters + */ +export abstract class BaseCluster extends cdk.Construct { /** - * The VPC this cluster was created in + * The VPC this cluster was created in. */ public readonly vpc: ec2.VpcNetworkRef; + /** + * The ARN of this cluster + */ public readonly clusterArn: string; + /** + * The name of this cluster + */ public readonly clusterName: string; constructor(parent: cdk.Construct, name: string, props: BaseClusterProps) { diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index dc332831af377..4ed25d03085fc 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -8,6 +8,9 @@ import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; import { ScalableTaskCount } from './scalable-task-count'; +/** + * Basic service properties + */ export interface BaseServiceProps { /** * Number of desired copies of running tasks @@ -59,13 +62,37 @@ export interface BaseServiceProps { platformVersion?: FargatePlatformVersion; } +/** + * Base class for Ecs and Fargate services + */ export abstract class BaseService extends cdk.Construct implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, cdk.IDependable { + + /** + * CloudFormation resources generated by this service + */ public readonly dependencyElements: cdk.IDependable[]; + + /** + * Manage allowed network traffic for this construct + */ public abstract readonly connections: ec2.Connections; + + /** + * ARN of this service + */ public readonly serviceArn: string; + + /** + * Name of this service + */ public readonly serviceName: string; + + /** + * Name of this service's cluster + */ public readonly clusterName: string; + protected loadBalancers = new Array(); protected networkConfiguration?: cloudformation.ServiceResource.NetworkConfigurationProperty; protected readonly abstract taskDef: BaseTaskDefinition; @@ -96,7 +123,10 @@ export abstract class BaseService extends cdk.Construct } /** - * FIXME How to reconcile this with the fact ECS registers service with target group automatically? + * Called when the service is attached to an ALB + * + * Don't call this function directly. Instead, call listener.addTarget() + * to add this service to a load balancer. */ public attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { const ret = this.attachToELBv2(targetGroup); @@ -110,10 +140,19 @@ export abstract class BaseService extends cdk.Construct return ret; } + /** + * Called when the service is attached to an NLB + * + * Don't call this function directly. Instead, call listener.addTarget() + * to add this service to a load balancer. + */ public attachToNetworkTargetGroup(targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { return this.attachToELBv2(targetGroup); } + /** + * SecurityGroup of this service + */ public get securityGroup(): ec2.SecurityGroupRef { return this._securityGroup!; } @@ -147,6 +186,9 @@ export abstract class BaseService extends cdk.Construct }); } + /** + * Set up AWSVPC networking for this construct + */ // tslint:disable-next-line:max-line-length protected configureAwsVpcNetworking(vpc: ec2.VpcNetworkRef, assignPublicIp?: boolean, vpcPlacement?: ec2.VpcPlacementStrategy, securityGroup?: ec2.SecurityGroupRef) { if (vpcPlacement === undefined) { @@ -167,6 +209,9 @@ export abstract class BaseService extends cdk.Construct }; } + /** + * Shared logic for attaching to an ELBv2 + */ private attachToELBv2(targetGroup: elbv2.ITargetGroup): elbv2.LoadBalancerTargetProps { if (this.taskDef.networkMode === NetworkMode.None) { throw new Error("Cannot use a load balancer if NetworkMode is None. Use Host or AwsVpc instead."); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index 3c253973275af..136af118cef98 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -3,6 +3,9 @@ import cdk = require('@aws-cdk/cdk'); import { ContainerDefinition, ContainerDefinitionProps } from '../container-definition'; import { cloudformation } from '../ecs.generated'; +/** + * Basic task definition properties + */ export interface BaseTaskDefinitionProps { /** * Namespace for task definition versions @@ -34,10 +37,28 @@ export interface BaseTaskDefinitionProps { volumes?: Volume[]; } +/** + * Base class for Ecs and Fargate task definitions + */ export abstract class BaseTaskDefinition extends cdk.Construct { + /** + * The family name of this task definition + */ public readonly family: string; + + /** + * ARN of this task definition + */ public readonly taskDefinitionArn: string; + + /** + * Task role used by this task definition + */ public readonly taskRole: iam.Role; + + /** + * Network mode used by this task definition + */ public abstract readonly networkMode: NetworkMode; /** @@ -48,8 +69,22 @@ export abstract class BaseTaskDefinition extends cdk.Construct { * container. */ public defaultContainer?: ContainerDefinition; + + /** + * All containers + */ private readonly containers = new Array(); + + /** + * All volumes + */ private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; + + /** + * Execution role for this task definition + * + * Will be created as needed. + */ private executionRole?: iam.Role; constructor(parent: cdk.Construct, name: string, props: BaseTaskDefinitionProps, additionalProps: any) { @@ -87,8 +122,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { } /** - * Add a container to this task - * FIXME pass in actual container instead of container props? + * Create a new container to this task definition */ public addContainer(id: string, props: ContainerDefinitionProps) { const container = new ContainerDefinition(this, id, this, props); @@ -103,6 +137,9 @@ export abstract class BaseTaskDefinition extends cdk.Construct { return container; } + /** + * Add a volume to this task definition + */ private addVolume(volume: Volume) { this.volumes.push(volume); } @@ -148,18 +185,43 @@ export enum NetworkMode { Host = 'host', } +/** + * Compatibilties + */ export enum Compatibilities { + /** + * EC2 capabilities + */ Ec2 = "EC2", + + /** + * Fargate capabilities + */ Fargate = "FARGATE" } -// FIXME separate Volume from InstanceVolume (Host not supported in Fargate) +/** + * Volume definition + */ export interface Volume { + /** + * Path on the host + */ host?: Host; + + /** + * A name for the volume + */ name?: string; // FIXME add dockerVolumeConfiguration } +/** + * A volume host + */ export interface Host { + /** + * Source path on the host + */ sourcePath?: string; } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index d5820a2169e39..bcbc8065b1044 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -5,6 +5,9 @@ import { cloudformation } from './ecs.generated'; import { LinuxParameters } from './linux-parameters'; import { LogDriver } from './log-drivers/log-driver'; +/** + * Properties of a container definition + */ export interface ContainerDefinitionProps { /** * The image to use for a container. @@ -169,23 +172,53 @@ export interface ContainerDefinitionProps { logging?: LogDriver; } +/** + * A definition for a single container in a Task + */ export class ContainerDefinition extends cdk.Construct { + /** + * Access Linux Parameters + */ public readonly linuxParameters = new LinuxParameters(); + /** + * The configured mount points + */ public readonly mountPoints = new Array(); + /** + * The configured port mappings + */ public readonly portMappings = new Array(); + /** + * The configured volumes + */ public readonly volumesFrom = new Array(); + /** + * The configured ulimits + */ public readonly ulimits = new Array(); + /** + * Whether or not this container is essential + */ public readonly essential: boolean; + /** + * The configured container links + */ private readonly links = new Array(); + /** + * The task definition this container definition is part of + */ private readonly taskDefinition: BaseTaskDefinition; + /** + * Whether this container uses an ECR image + */ private _usesEcrImages: boolean = false; constructor(parent: cdk.Construct, id: string, taskDefinition: BaseTaskDefinition, private readonly props: ContainerDefinitionProps) { @@ -195,6 +228,9 @@ export class ContainerDefinition extends cdk.Construct { props.image.bind(this); } + /** + * Add a link from this container to a different container + */ public addLink(container: ContainerDefinition, alias?: string) { if (alias !== undefined) { this.links.push(`${container.id}:${alias}`); @@ -203,10 +239,16 @@ export class ContainerDefinition extends cdk.Construct { } } + /** + * Add one or more mount points to this container + */ public addMountPoints(...mountPoints: MountPoint[]) { this.mountPoints.push(...mountPoints); } + /** + * Add one or more port mappings to this container + */ public addPortMappings(...portMappings: PortMapping[]) { for (const pm of portMappings) { if (this.taskDefinition.networkMode === NetworkMode.AwsVpc || this.taskDefinition.networkMode === NetworkMode.Host) { @@ -223,10 +265,16 @@ export class ContainerDefinition extends cdk.Construct { this.portMappings.push(...portMappings); } + /** + * Add one or more ulimits to this container + */ public addUlimits(...ulimits: Ulimit[]) { this.ulimits.push(...ulimits); } + /** + * Add one or more volumes to this container + */ public addVolumesFrom(...volumesFrom: VolumeFrom[]) { this.volumesFrom.push(...volumesFrom); } @@ -238,16 +286,19 @@ export class ContainerDefinition extends cdk.Construct { this._usesEcrImages = true; } + /** + * Whether this container uses ECR images + */ public get usesEcrImages() { return this._usesEcrImages; } /** - * Ingress Port is needed to set the security group ingress for the task/service. + * Ingress Port is needed to set the security group ingress for the task/service */ public get ingressPort(): number { if (this.portMappings.length === 0) { - throw new Error(`Container ${this.id} hasn't defined any ports`); + throw new Error(`Container ${this.id} hasn't defined any ports. Call addPortMappings().`); } const defaultPortMapping = this.portMappings[0]; @@ -260,17 +311,21 @@ export class ContainerDefinition extends cdk.Construct { } return defaultPortMapping.containerPort; } + /** * Return the port that the container will be listening on by default */ public get containerPort(): number { if (this.portMappings.length === 0) { - throw new Error(`Container ${this.id} hasn't defined any ports`); + throw new Error(`Container ${this.id} hasn't defined any ports. Call addPortMappings().`); } const defaultPortMapping = this.portMappings[0]; return defaultPortMapping.containerPort; } + /** + * Render this container definition to a CloudFormation object + */ public renderContainerDefinition(): cloudformation.TaskDefinitionResource.ContainerDefinitionProperty { return { command: this.props.command, @@ -394,16 +449,32 @@ function getHealthCheckCommand(hc: HealthCheck): string[] { } /** - * Container ulimits. Correspond to ulimits options on docker run. + * Container ulimits. + * + * Correspond to ulimits options on docker run. * * NOTE: Does not work for Windows containers. */ export interface Ulimit { + /** + * What resource to enforce a limit on + */ name: UlimitName, + + /** + * Soft limit of the resource + */ softLimit: number, + + /** + * Hard limit of the resource + */ hardLimit: number, } +/** + * Type of resource to set a limit on + */ export enum UlimitName { Core = "core", Cpu = "cpu", @@ -458,8 +529,18 @@ export interface PortMapping { protocol?: Protocol } +/** + * Network protocol + */ export enum Protocol { + /** + * TCP + */ Tcp = "tcp", + + /** + * UDP + */ Udp = "udp", } @@ -485,9 +566,19 @@ function renderMountPoint(mp: MountPoint): cloudformation.TaskDefinitionResource }; } +/** + * A volume from another container + */ export interface VolumeFrom { - sourceContainer: string, - readOnly: boolean, + /** + * Name of the source container + */ + sourceContainer: string, + + /** + * Whether the volume is read only + */ + readOnly: boolean, } function renderVolumeFrom(vf: VolumeFrom): cloudformation.TaskDefinitionResource.VolumeFromProperty { diff --git a/packages/@aws-cdk/aws-ecs/lib/container-image.ts b/packages/@aws-cdk/aws-ecs/lib/container-image.ts index 25ecc9fb5590a..e5131f7a7d42d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-image.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-image.ts @@ -1,16 +1,35 @@ import { ContainerDefinition } from './container-definition'; +/** + * Base class for container images + */ export abstract class ContainerImage { + /** + * Name of the image + */ public abstract readonly imageName: string; + + /** + * Called when the image is used by a ContainerDefinition + */ public abstract bind(containerDefinition: ContainerDefinition): void; } +/** + * Factory for DockerHub images + */ export class DockerHub { + /** + * Reference an image on DockerHub + */ public static image(name: string): ContainerImage { return new DockerHubImage(name); } } +/** + * A DockerHub image + */ class DockerHubImage { constructor(public readonly imageName: string) { } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts index 577169eaceb2c..64c9834c63e47 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts @@ -5,6 +5,9 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; +/** + * Properties to define an ECS cluster + */ export interface EcsClusterProps extends BaseClusterProps { /** * Whether or not the containers can access the instance role @@ -26,12 +29,25 @@ export interface EcsClusterProps extends BaseClusterProps { size?: number; } +/** + * A container cluster that runs on your EC2 instances + */ export class EcsCluster extends BaseCluster implements IEcsCluster { + /** + * Import an existing cluster + */ public static import(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps): IEcsCluster { return new ImportedEcsCluster(parent, name, props); } + /** + * The AutoScalingGroup that the cluster is running on + */ public readonly autoScalingGroup: autoscaling.AutoScalingGroup; + + /** + * SecurityGroup of the EC2 instances + */ public readonly securityGroup: ec2.SecurityGroupRef; constructor(parent: cdk.Construct, name: string, props: EcsClusterProps) { @@ -59,23 +75,6 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { autoScalingGroup.addUserData('sudo service iptables save'); } - // Note: if the ASG doesn't launch or doesn't register itself with - // ECS, *Cluster* stabilization will fail after timing our for an hour - // or so, because the *Service* doesn't have any running instances. - // During this time, you CANNOT DO ANYTHING ELSE WITH YOUR STACK. - // - // Apart from the weird relationship here between Cluster and Service - // (why is Cluster failing and not Service?), the experience is... - // - // NOT GREAT. - // - // Also, there's sort of a bidirectional dependency between Cluster and ASG: - // - // - ASG depends on Cluster to get the ClusterName (which needs to go into - // UserData). - // - Cluster depends on ASG to boot up, so the service is launched, so the - // Cluster can stabilize. - // ECS instances must be able to do these things // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html autoScalingGroup.addToRolePolicy(new iam.PolicyStatement().addActions( @@ -97,8 +96,6 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { this.autoScalingGroup = autoScalingGroup; } - // TODO Add cluster scaling API - /** * Export the EcsCluster */ @@ -147,6 +144,9 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { export class EcsOptimizedAmi implements ec2.IMachineImageSource { private static AmiParameterName = "/aws/service/ecs/optimized-ami/amazon-linux/recommended"; + /** + * Return the correct image + */ public getImage(parent: cdk.Construct): ec2.MachineImage { const ssmProvider = new cdk.SSMParameterProvider(parent, { parameterName: EcsOptimizedAmi.AmiParameterName @@ -159,24 +159,63 @@ export class EcsOptimizedAmi implements ec2.IMachineImageSource { } } +/** + * An ECS cluster + */ export interface IEcsCluster { + /** + * Name of the cluster + */ readonly clusterName: string; + + /** + * VPC that the cluster instances are running in + */ readonly vpc: ec2.VpcNetworkRef; + + /** + * Security group of the cluster instances + */ readonly securityGroup: ec2.SecurityGroupRef; } +/** + * Properties to import an ECS cluster + */ export interface ImportedEcsClusterProps { - readonly clusterName: string; - readonly vpc: ec2.VpcNetworkRefProps; - readonly securityGroup: ec2.SecurityGroupRefProps; + /** + * Name of the cluster + */ + clusterName: string; + + /** + * VPC that the cluster instances are running in + */ + vpc: ec2.VpcNetworkRefProps; + + /** + * Security group of the cluster instances + */ + securityGroup: ec2.SecurityGroupRefProps; } -// /** -// * A EcsCluster that has been imported -// */ +/** + * An EcsCluster that has been imported + */ class ImportedEcsCluster extends cdk.Construct implements IEcsCluster { + /** + * Name of the cluster + */ public readonly clusterName: string; + + /** + * VPC that the cluster instances are running in + */ public readonly vpc: ec2.VpcNetworkRef; + + /** + * Security group of the cluster instances + */ public readonly securityGroup: ec2.SecurityGroupRef; constructor(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps) { diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts index f0c6c1a80abe1..e472884657d66 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts @@ -8,11 +8,14 @@ import { cloudformation } from '../ecs.generated'; import { IEcsCluster } from './ecs-cluster'; import { EcsTaskDefinition } from './ecs-task-definition'; +/** + * Properties to define an ECS service + */ export interface EcsServiceProps extends BaseServiceProps { /** * Cluster where service will be deployed */ - cluster: IEcsCluster; // should be required? do we assume 'default' exists? + cluster: IEcsCluster; /** * Task Definition used for running tasks in the service @@ -55,10 +58,22 @@ export interface EcsServiceProps extends BaseServiceProps { daemon?: boolean; } +/** + * Start a service on an ECS cluster + */ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { + /** + * Manage allowed network traffic for this construct + */ public readonly connections: ec2.Connections; + + /** + * Name of the cluster + */ public readonly clusterName: string; + protected readonly taskDef: BaseTaskDefinition; + private readonly taskDefinition: EcsTaskDefinition; private readonly constraints: cloudformation.ServiceResource.PlacementConstraintProperty[]; private readonly strategies: cloudformation.ServiceResource.PlacementStrategyProperty[]; @@ -215,6 +230,9 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { } } +/** + * Validate combinations of networking arguments + */ function validateNoNetworkingProps(props: EcsServiceProps) { if (props.vpcPlacement !== undefined || props.securityGroup !== undefined) { throw new Error('vpcPlacement and securityGroup can only be used in AwsVpc networking mode'); diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts index 19659836781d9..aea2031e341c2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts @@ -2,6 +2,9 @@ import cdk = require('@aws-cdk/cdk'); import { BaseTaskDefinition, BaseTaskDefinitionProps, Compatibilities, NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; +/** + * Properties to define an ECS task definition + */ export interface EcsTaskDefinitionProps extends BaseTaskDefinitionProps { /** * The Docker networking mode to use for the containers in the task. @@ -20,8 +23,18 @@ export interface EcsTaskDefinitionProps extends BaseTaskDefinitionProps { placementConstraints?: PlacementConstraint[]; } +/** + * Define Tasks to run on an ECS cluster + */ export class EcsTaskDefinition extends BaseTaskDefinition { + /** + * The networkmode configuration of this task + */ public readonly networkMode: NetworkMode; + + /** + * Placement constraints for task instances + */ private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[]; constructor(parent: cdk.Construct, name: string, props: EcsTaskDefinitionProps = {}) { @@ -49,6 +62,9 @@ export class EcsTaskDefinition extends BaseTaskDefinition { this.placementConstraints.push(pc); } + /** + * Render the placement constraints + */ private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { return { type: pc.type, @@ -57,12 +73,32 @@ export class EcsTaskDefinition extends BaseTaskDefinition { } } +/** + * A constraint on how instances should be placed + */ export interface PlacementConstraint { - expression?: string; + /** + * The type of constraint + */ type: PlacementConstraintType; + + /** + * Additional information for the constraint + */ + expression?: string; } +/** + * A placement constraint type + */ export enum PlacementConstraintType { + /** + * Place each task on a different instance + */ DistinctInstance = "distinctInstance", - MemberOf = "memberOf" + + /** + * Place tasks only on instances matching the expression in 'expression' + */ + MemberOf = "memberOf" } diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts index a92f990657592..a720237127eea 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts @@ -2,17 +2,28 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; +/** + * Properties to define a Fargate cluster + */ // tslint:disable-next-line:no-empty-interface export interface FargateClusterProps extends BaseClusterProps { } +/** + * Define a cluster to run tasks on managed instances + */ export class FargateCluster extends BaseCluster implements IFargateCluster { + /** + * Import an existing Fargate cluster + */ public static import(parent: cdk.Construct, name: string, props: ImportedFargateClusterProps): IFargateCluster { return new ImportedFargateCluster(parent, name, props); } + constructor(parent: cdk.Construct, name: string, props: FargateClusterProps) { super(parent, name, props); } + /** * Export the FargateCluster */ @@ -24,21 +35,48 @@ export class FargateCluster extends BaseCluster implements IFargateCluster { } } +/** + * A Fargate cluster + */ export interface IFargateCluster { - clusterName: string; - vpc: ec2.VpcNetworkRef; + /** + * Name of the cluster + */ + readonly clusterName: string; + + /** + * VPC where Task ENIs will be placed + */ + readonly vpc: ec2.VpcNetworkRef; } +/** + * Properties to import a Fargate cluster + */ export interface ImportedFargateClusterProps { - readonly clusterName: string; - readonly vpc: ec2.VpcNetworkRefProps; + /** + * Name of the cluster + */ + clusterName: string; + + /** + * VPC where Task ENIs should be placed + */ + vpc: ec2.VpcNetworkRefProps; } -// /** -// * A FargateCluster that has been imported -// */ +/** + * A FargateCluster that has been imported + */ class ImportedFargateCluster extends cdk.Construct implements IFargateCluster { + /** + * Name of the cluster + */ public readonly clusterName: string; + + /** + * VPC where ENIs will be placed + */ public readonly vpc: ec2.VpcNetworkRef; constructor(parent: cdk.Construct, name: string, props: ImportedFargateClusterProps) { diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index b086893b0e1f8..d3ea11ef4a659 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -5,6 +5,9 @@ import { BaseTaskDefinition } from '../base/base-task-definition'; import { IFargateCluster } from './fargate-cluster'; import { FargateTaskDefinition } from './fargate-task-definition'; +/** + * Properties to define a Fargate service + */ export interface FargateServiceProps extends BaseServiceProps { /** * Cluster where service will be deployed @@ -38,8 +41,18 @@ export interface FargateServiceProps extends BaseServiceProps { securityGroup?: ec2.SecurityGroupRef; } +/** + * Start a service on an ECS cluster + */ export class FargateService extends BaseService { + /** + * Manage allowed network traffic for this construct + */ public readonly connections: ec2.Connections; + + /** + * The Task Definition for this service + */ public readonly taskDefinition: FargateTaskDefinition; protected readonly taskDef: BaseTaskDefinition; diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index 4dd95a84319a2..37d2eba0a4174 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -1,6 +1,9 @@ import cdk = require('@aws-cdk/cdk'); import { BaseTaskDefinition, BaseTaskDefinitionProps, Compatibilities, NetworkMode } from '../base/base-task-definition'; +/** + * Properties to define a Fargate Task + */ export interface FargateTaskDefinitionProps extends BaseTaskDefinitionProps { /** * The number of cpu units used by the task. @@ -36,7 +39,13 @@ export interface FargateTaskDefinitionProps extends BaseTaskDefinitionProps { memoryMiB?: string; } +/** + * A definition for Tasks on a Fargate cluster + */ export class FargateTaskDefinition extends BaseTaskDefinition { + /** + * The configured network mode + */ public readonly networkMode = NetworkMode.AwsVpc; constructor(parent: cdk.Construct, name: string, props: FargateTaskDefinitionProps = {}) { diff --git a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts index bdc7c9a7c0089..e3cdb39ba74f2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts +++ b/packages/@aws-cdk/aws-ecs/lib/linux-parameters.ts @@ -1,37 +1,74 @@ import { cloudformation } from './ecs.generated'; +/** + * Linux parameter setup in a container + */ export class LinuxParameters { + /** + * Whether the init process is enabled + */ public initProcessEnabled?: boolean; + /** + * The shared memory size + */ public sharedMemorySize?: number; + /** + * Capabilities to be added + */ private readonly capAdd: Capability[] = []; + /** + * Capabilities to be dropped + */ private readonly capDrop: Capability[] = []; + /** + * Device mounts + */ private readonly devices: Device[] = []; + /** + * TMPFS mounts + */ private readonly tmpfs: Tmpfs[] = []; /** - * AddCapabilities only works with EC2 launch type + * Add one or more capabilities + * + * Only works with EC2 launch type. */ public addCapabilities(...cap: Capability[]) { this.capAdd.push(...cap); } + /** + * Drop one or more capabilities + * + * Only works with EC2 launch type. + */ public dropCapabilities(...cap: Capability[]) { this.capDrop.push(...cap); } + /** + * Add one or more devices + */ public addDevices(...device: Device[]) { this.devices.push(...device); } + /** + * Add one or more tmpfs mounts + */ public addTmpfs(...tmpfs: Tmpfs[]) { this.tmpfs.push(...tmpfs); } + /** + * Render the Linux parameters to a CloudFormation object + */ public renderLinuxParameters(): cloudformation.TaskDefinitionResource.LinuxParametersProperty { return { initProcessEnabled: this.initProcessEnabled, @@ -46,9 +83,27 @@ export class LinuxParameters { } } +/** + * A host device + */ export interface Device { + /** + * Path in the container + * + * @default Same path as the host + */ containerPath?: string, + + /** + * Path on the host + */ hostPath: string, + + /** + * Permissions + * + * @default Readonly + */ permissions?: DevicePermission[] } @@ -60,9 +115,23 @@ function renderDevice(device: Device): cloudformation.TaskDefinitionResource.Dev }; } +/** + * A tmpfs mount + */ export interface Tmpfs { + /** + * Path in the container to mount + */ containerPath: string, + + /** + * Size of the volume + */ size: number, + + /** + * Mount options + */ mountOptions?: TmpfsMountOption[], } @@ -74,6 +143,9 @@ function renderTmpfs(tmpfs: Tmpfs): cloudformation.TaskDefinitionResource.TmpfsP }; } +/** + * A Linux capability + */ export enum Capability { All = "ALL", AuditControl = "AUDIT_CONTROL", @@ -115,12 +187,29 @@ export enum Capability { WakeAlarm = "WAKE_ALARM" } +/** + * Permissions for device access + */ export enum DevicePermission { + /** + * Read + */ Read = "read", + + /** + * Write + */ Write = "write", + + /** + * Make a node + */ Mknod = "mknod", } +/** + * Options for a tmpfs mount + */ export enum TmpfsMountOption { Defaults = "defaults", Ro = "ro", diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts index 839263d6e927e..bc24316ffb302 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts @@ -5,6 +5,9 @@ import { IEcsCluster } from './ecs/ecs-cluster'; import { EcsService } from './ecs/ecs-service'; import { EcsTaskDefinition } from './ecs/ecs-task-definition'; +/** + * Properties for a LoadBalancedEcsService + */ export interface LoadBalancedEcsServiceProps { /** * The cluster where your Fargate service will be deployed @@ -12,10 +15,7 @@ export interface LoadBalancedEcsServiceProps { cluster: IEcsCluster; /** - * The image to use for a container. - * - * You can use images in the Docker Hub registry or specify other - * repositories (repository-url/image:tag). + * The image to start. */ image: ContainerImage; @@ -56,7 +56,13 @@ export interface LoadBalancedEcsServiceProps { publicLoadBalancer?: boolean; } +/** + * A single task running on an ECS cluster fronted by a load balancer + */ export class LoadBalancedEcsService extends cdk.Construct { + /** + * The load balancer that is fronting the ECS service + */ public readonly loadBalancer: elbv2.ApplicationLoadBalancer; constructor(parent: cdk.Construct, id: string, props: LoadBalancedEcsServiceProps) { @@ -91,5 +97,7 @@ export class LoadBalancedEcsService extends cdk.Construct { port: 80, targets: [service] }); + + new cdk.Output(this, 'LoadBalancerDNS', { value: lb.dnsName }); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts index 23fea36a3729c..274a664ff6ef4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts @@ -4,8 +4,15 @@ import { DockerHub } from './container-image'; import { FargateCluster } from './fargate/fargate-cluster'; import { LoadBalancedFargateService } from './load-balanced-fargate-service'; +/** + * Properties for a LoadBalancedEcsServiceApplet + */ export interface LoadBalancedFargateServiceAppletProps extends cdk.StackProps { + /** + * The image to start (from DockerHub) + */ image: string; + /** * The number of cpu units used by the task. * Valid values, which determines your range of valid values for the memory parameter: @@ -65,6 +72,9 @@ export interface LoadBalancedFargateServiceAppletProps extends cdk.StackProps { publicTasks?: boolean; } +/** + * An applet for a LoadBalancedFargateService + */ export class LoadBalancedFargateServiceApplet extends cdk.Stack { constructor(parent: cdk.App, id: string, props: LoadBalancedFargateServiceAppletProps) { super(parent, id, props); diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts index 04cae422cc05d..7275de17d7841 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts @@ -5,13 +5,20 @@ import { IFargateCluster } from './fargate/fargate-cluster'; import { FargateService } from './fargate/fargate-service'; import { FargateTaskDefinition } from './fargate/fargate-task-definition'; +/** + * Properties for a LoadBalancedEcsService + */ export interface LoadBalancedFargateServiceProps { /** * The cluster where your Fargate service will be deployed */ cluster: IFargateCluster; + /** + * The image to start + */ image: ContainerImage; + /** * The number of cpu units used by the task. * Valid values, which determines your range of valid values for the memory parameter: @@ -71,6 +78,9 @@ export interface LoadBalancedFargateServiceProps { publicTasks?: boolean; } +/** + * A single task running on an ECS cluster fronted by a load balancer + */ export class LoadBalancedFargateService extends cdk.Construct { public readonly loadBalancer: elbv2.ApplicationLoadBalancer; @@ -110,5 +120,7 @@ export class LoadBalancedFargateService extends cdk.Construct { port: 80, targets: [service] }); + + new cdk.Output(this, 'LoadBalancerDNS', { value: lb.dnsName }); } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 28869a680a345..a0cfc357e6bec 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -54,6 +54,7 @@ "devDependencies": { "@aws-cdk/assert": "^0.14.1", "cdk-build-tools": "^0.14.1", + "cdk-integ-tools": "^0.14.1", "cfn2ts": "^0.14.1", "pkglint": "^0.14.1" }, diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts index 3b2c29c76065f..3af4c35645bf6 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts @@ -1,5 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); +import elb = require('@aws-cdk/aws-elasticloadbalancing'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import ecs = require('../../lib'); @@ -387,5 +388,37 @@ export = { test.done(); } + }, + + 'classic ELB': { + 'can attach to classic ELB'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TD', { networkMode: ecs.NetworkMode.Host }); + const container = taskDefinition.addContainer('web', { + image: ecs.DockerHub.image('test'), + }); + container.addPortMappings({ containerPort: 808 }); + const service = new ecs.EcsService(stack, 'Service', { cluster, taskDefinition }); + + // WHEN + const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); + lb.addTarget(service); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: "web", + ContainerPort: 808, + LoadBalancerName: { Ref: "LB8A12904C" } + } + ], + })); + + test.done(); + }, } }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index afd6ab836f168..19819a1ca3cce 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -1,3 +1,4 @@ +import { expect, haveResource } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import ecs = require('../lib'); @@ -206,4 +207,34 @@ export = { }, }, }, + + 'can add AWS logging to container definition'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef'); + + // WHEN + taskDefinition.addContainer('cont', { + image: ecs.DockerHub.image('test'), + logging: new ecs.AwsLogDriver(stack, 'Logging', { streamPrefix: 'prefix' }) + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + LogConfiguration: { + LogDriver: "awslogs", + Options: { + "awslogs-group": { Ref: "LoggingLogGroupC6B8E20B" }, + "awslogs-stream-prefix": "prefix", + "awslogs-region": { Ref: "AWS::Region" } + } + }, + } + ] + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-ecs/test/test.ecs.ts b/packages/@aws-cdk/aws-ecs/test/test.ecs.ts deleted file mode 100644 index 820f6b467f38f..0000000000000 --- a/packages/@aws-cdk/aws-ecs/test/test.ecs.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Test, testCase } from 'nodeunit'; - -export = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); - test.done(); - } -}); diff --git a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts new file mode 100644 index 0000000000000..2ce7bcf1654c3 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts @@ -0,0 +1,43 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../lib'); + +export = { + 'test ECS loadbalanced construct'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecs.LoadBalancedEcsService(stack, 'Service', { + cluster, + image: ecs.DockerHub.image('test') + }); + + // THEN - stack containers a load balancer + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer')); + + test.done(); + }, + + 'test Fargateloadbalanced construct'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const cluster = new ecs.FargateCluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecs.LoadBalancedFargateService(stack, 'Service', { + cluster, + image: ecs.DockerHub.image('test') + }); + + // THEN - stack containers a load balancer + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer')); + + test.done(); + } +}; \ No newline at end of file diff --git a/tools/cdk-integ-tools/lib/integ-helpers.ts b/tools/cdk-integ-tools/lib/integ-helpers.ts index 9a48d4d856e6a..2582b3f5f78cf 100644 --- a/tools/cdk-integ-tools/lib/integ-helpers.ts +++ b/tools/cdk-integ-tools/lib/integ-helpers.ts @@ -5,6 +5,9 @@ import fs = require('fs'); import path = require('path'); import util = require('util'); +const stat = util.promisify(fs.stat); +const readdir = util.promisify(fs.readdir); + export class IntegrationTests { constructor(private readonly directory: string) { } @@ -18,14 +21,33 @@ export class IntegrationTests { } public async discover(): Promise { - const files = await util.promisify(fs.readdir)(this.directory); - const integs = files.filter(fileName => fileName.startsWith('integ.') && fileName.endsWith('.js')); + const files = await this.readTree(); + const integs = files.filter(fileName => path.basename(fileName).startsWith('integ.') && path.basename(fileName).endsWith('.js')); return await this.request(integs); } public async request(files: string[]): Promise { return files.map(fileName => new IntegrationTest(this.directory, fileName)); } + + private async readTree(): Promise { + const ret = new Array(); + + const rootDir = this.directory; + + async function recurse(dir: string) { + const files = await readdir(dir); + for (const file of files) { + const fullPath = path.join(dir, file); + const statf = await stat(fullPath); + if (statf.isFile()) { ret.push(fullPath.substr(rootDir.length + 1)); } + if (statf.isDirectory()) { await recurse(path.join(fullPath)); } + } + } + + await recurse(this.directory); + return ret; + } } export class IntegrationTest { From 9825127053eece62565a371f92292012cf487ffa Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 30 Oct 2018 16:24:37 -0700 Subject: [PATCH 101/140] Unit test for error case on service creation --- .../test/fargate/test.fargate-service.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts index 9eaa639f394bf..30132983d62a7 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -81,6 +81,24 @@ export = { test.done(); }, + "errors when no container specified on task definition"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.FargateCluster(stack, 'FargateCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + // THEN + test.throws(() => { + new ecs.FargateService(stack, "FargateService", { + cluster, + taskDefinition, + }); + }); + + test.done(); + }, + "allows specifying assignPublicIP as enabled"(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -96,7 +114,6 @@ export = { cluster, taskDefinition, assignPublicIp: true - }); // THEN From 15755d0a05ca1d92d6ef5d95356050060a2b4b67 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 31 Oct 2018 23:16:49 +0100 Subject: [PATCH 102/140] Add integ test expectations --- .../test/integ.lb-awsvpc-nw.expected.json | 634 ++++++++++++++++ .../test/integ.lb-bridge-nw.expected.json | 691 ++++++++++++++++++ tools/cdk-integ-tools/lib/integ-helpers.ts | 1 + 3 files changed, 1326 insertions(+) create mode 100644 packages/@aws-cdk/aws-ecs/test/integ.lb-awsvpc-nw.expected.json create mode 100644 packages/@aws-cdk/aws-ecs/test/integ.lb-bridge-nw.expected.json diff --git a/packages/@aws-cdk/aws-ecs/test/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/integ.lb-awsvpc-nw.expected.json new file mode 100644 index 0000000000000..d4f7ba85f8c29 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/integ.lb-awsvpc-nw.expected.json @@ -0,0 +1,634 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "FargateCluster7CCD5F93": { + "Type": "AWS::ECS::Cluster" + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "Links": [], + "LinuxParameters": { + "Capabilities": { + "Add": [], + "Drop": [] + }, + "Devices": [], + "Tmpfs": [] + }, + "MountPoints": [], + "Name": "web", + "PortMappings": [ + { + "ContainerPort": 80, + "Protocol": "tcp" + } + ], + "Ulimits": [], + "VolumesFrom": [] + } + ], + "Cpu": "512", + "Family": "awsecsintegTaskDef6FDFB69A", + "Memory": "1GB", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + }, + "Volumes": [] + } + }, + "ServiceD69D759B": { + "Type": "AWS::ECS::Service", + "Properties": { + "TaskDefinition": { + "Ref": "TaskDef54694570" + }, + "Cluster": { + "Ref": "FargateCluster7CCD5F93" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DesiredCount": 1, + "LaunchType": "FARGATE", + "LoadBalancers": [ + { + "ContainerName": "web", + "ContainerPort": 80, + "TargetGroupArn": { + "Ref": "LBPublicListenerFargateGroup5EE2FBAF" + } + } + ], + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "ServiceSecurityGroupC96ED6A7", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + } + } + }, + "DependsOn": [ + "LBPublicListener6E1F3D94" + ] + }, + "ServiceSecurityGroupC96ED6A7": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ/Service/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "ServiceSecurityGroupfromawsecsintegLBSecurityGroupC30F5EB480CD1B9463": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "FromPort": 80, + "GroupId": { + "Fn::GetAtt": [ + "ServiceSecurityGroupC96ED6A7", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + }, + "ToPort": 80 + } + }, + "ServiceTaskCountTarget23E25614": { + "Type": "AWS::ApplicationAutoScaling::ScalableTarget", + "Properties": { + "MaxCapacity": 10, + "MinCapacity": 1, + "ResourceId": { + "Fn::Join": [ + "", + [ + "service/", + { + "Ref": "FargateCluster7CCD5F93" + }, + "/", + { + "Fn::GetAtt": [ + "ServiceD69D759B", + "Name" + ] + } + ] + ] + }, + "RoleARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService" + ] + ] + }, + "ScalableDimension": "ecs:service:DesiredCount", + "ServiceNamespace": "ecs", + "ScheduledActions": [] + } + }, + "ServiceTaskCountTargetReasonableCpu4174EFCE": { + "Type": "AWS::ApplicationAutoScaling::ScalingPolicy", + "Properties": { + "PolicyName": "awsecsintegServiceTaskCountTargetReasonableCpuDB6AEA73", + "PolicyType": "TargetTrackingScaling", + "ScalingTargetId": { + "Ref": "ServiceTaskCountTarget23E25614" + }, + "TargetTrackingScalingPolicyConfiguration": { + "PredefinedMetricSpecification": { + "PredefinedMetricType": "ECSServiceAverageCPUUtilization" + }, + "TargetValue": 10 + } + } + }, + "LB8A12904C": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "LoadBalancerAttributes": [], + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + ], + "Type": "application" + } + }, + "LBSecurityGroup8A41EA2B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB awsecsintegLBC73915FE", + "SecurityGroupEgress": [], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow from anyone on port 80", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "LBSecurityGrouptoawsecsintegServiceSecurityGroup48EE4368807B287D7F": { + "Type": "AWS::EC2::SecurityGroupEgress", + "Properties": { + "GroupId": { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + }, + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "ServiceSecurityGroupC96ED6A7", + "GroupId" + ] + }, + "FromPort": 80, + "ToPort": 80 + } + }, + "LBPublicListener6E1F3D94": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "LBPublicListenerFargateGroup5EE2FBAF" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "LB8A12904C" + }, + "Port": 80, + "Protocol": "HTTP", + "Certificates": [] + } + }, + "LBPublicListenerFargateGroup5EE2FBAF": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "TargetGroupAttributes": [], + "Targets": [], + "TargetType": "ip" + } + } + }, + "Outputs": { + "LoadBalancerDNS": { + "Value": { + "Fn::GetAtt": [ + "LB8A12904C", + "DNSName" + ] + }, + "Export": { + "Name": "aws-ecs-integ:LoadBalancerDNS" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/integ.lb-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/integ.lb-bridge-nw.expected.json new file mode 100644 index 0000000000000..cb0b9b13a92d2 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/integ.lb-bridge-nw.expected.json @@ -0,0 +1,691 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "EcsCluster97242B84": { + "Type": "AWS::ECS::Cluster" + }, + "EcsClusterAutoScalingGroupInstanceSecurityGroupBFB09B50": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ-ecs/EcsCluster/AutoScalingGroup/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/EcsCluster/AutoScalingGroup" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "EcsClusterAutoScalingGroupInstanceSecurityGroupfromawsecsintegecsLBSecurityGroup7DA90129808022F17D5A": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "FromPort": 8080, + "GroupId": { + "Fn::GetAtt": [ + "EcsClusterAutoScalingGroupInstanceSecurityGroupBFB09B50", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + }, + "ToPort": 8080 + } + }, + "EcsClusterAutoScalingGroupInstanceRole2505B71D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "EcsClusterAutoScalingGroupInstanceRoleDefaultPolicy0DEB9F1F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EcsClusterAutoScalingGroupInstanceRoleDefaultPolicy0DEB9F1F", + "Roles": [ + { + "Ref": "EcsClusterAutoScalingGroupInstanceRole2505B71D" + } + ] + } + }, + "EcsClusterAutoScalingGroupInstanceProfile77D897B8": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "EcsClusterAutoScalingGroupInstanceRole2505B71D" + } + ] + } + }, + "EcsClusterAutoScalingGroupLaunchConfig965E00BD": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": "ami-1234", + "InstanceType": "t2.micro", + "IamInstanceProfile": { + "Ref": "EcsClusterAutoScalingGroupInstanceProfile77D897B8" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "EcsClusterAutoScalingGroupInstanceSecurityGroupBFB09B50", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "Ref": "EcsCluster97242B84" + }, + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save" + ] + ] + } + } + }, + "DependsOn": [ + "EcsClusterAutoScalingGroupInstanceRole2505B71D", + "EcsClusterAutoScalingGroupInstanceRoleDefaultPolicy0DEB9F1F" + ] + }, + "EcsClusterAutoScalingGroupASGB16C0B67": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "0", + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "EcsClusterAutoScalingGroupLaunchConfig965E00BD" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-ecs-integ-ecs/EcsCluster/AutoScalingGroup" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "Links": [], + "LinuxParameters": { + "Capabilities": { + "Add": [], + "Drop": [] + }, + "Devices": [], + "Tmpfs": [] + }, + "Memory": 256, + "MountPoints": [], + "Name": "web", + "PortMappings": [ + { + "ContainerPort": 80, + "HostPort": 8080, + "Protocol": "tcp" + } + ], + "Ulimits": [], + "VolumesFrom": [] + } + ], + "Family": "awsecsintegecsTaskDef8DD0C801", + "NetworkMode": "bridge", + "PlacementConstraints": [], + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + }, + "Volumes": [] + } + }, + "ServiceD69D759B": { + "Type": "AWS::ECS::Service", + "Properties": { + "TaskDefinition": { + "Ref": "TaskDef54694570" + }, + "Cluster": { + "Ref": "EcsCluster97242B84" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DesiredCount": 1, + "LaunchType": "EC2", + "LoadBalancers": [ + { + "ContainerName": "web", + "ContainerPort": 80, + "TargetGroupArn": { + "Ref": "LBPublicListenerECSGroupD6A32205" + } + } + ], + "PlacementConstraints": [], + "PlacementStrategies": [], + "SchedulingStrategy": "REPLICA" + }, + "DependsOn": [ + "LBPublicListener6E1F3D94" + ] + }, + "LB8A12904C": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "LoadBalancerAttributes": [], + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + ], + "Type": "application" + } + }, + "LBSecurityGroup8A41EA2B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB awsecsintegecsLB84BFA683", + "SecurityGroupEgress": [], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow from anyone on port 80", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "LBSecurityGrouptoawsecsintegecsEcsClusterAutoScalingGroupInstanceSecurityGroup019ACCA48080512AF7A6": { + "Type": "AWS::EC2::SecurityGroupEgress", + "Properties": { + "GroupId": { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + }, + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "EcsClusterAutoScalingGroupInstanceSecurityGroupBFB09B50", + "GroupId" + ] + }, + "FromPort": 8080, + "ToPort": 8080 + } + }, + "LBPublicListener6E1F3D94": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "LBPublicListenerECSGroupD6A32205" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "LB8A12904C" + }, + "Port": 80, + "Protocol": "HTTP", + "Certificates": [] + } + }, + "LBPublicListenerECSGroupD6A32205": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "TargetGroupAttributes": [], + "Targets": [], + "TargetType": "instance" + } + } + }, + "Outputs": { + "LoadBalancerDNS": { + "Value": { + "Fn::GetAtt": [ + "LB8A12904C", + "DNSName" + ] + }, + "Export": { + "Name": "aws-ecs-integ-ecs:LoadBalancerDNS" + } + } + } +} \ No newline at end of file diff --git a/tools/cdk-integ-tools/lib/integ-helpers.ts b/tools/cdk-integ-tools/lib/integ-helpers.ts index 2582b3f5f78cf..4cbab62415bce 100644 --- a/tools/cdk-integ-tools/lib/integ-helpers.ts +++ b/tools/cdk-integ-tools/lib/integ-helpers.ts @@ -112,6 +112,7 @@ export const STATIC_TEST_CONTEXT = { [DEFAULT_REGION_CONTEXT_KEY]: "test-region", "availability-zones:account=12345678:region=test-region": [ "test-region-1a", "test-region-1b", "test-region-1c" ], "ssm:account=12345678:parameterName=/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2:region=test-region": "ami-1234", + "ssm:account=12345678:parameterName=/aws/service/ecs/optimized-ami/amazon-linux/recommended:region=test-region": "{\"image_id\": \"ami-1234\"}", }; /** From 7faa05d1cde2b1d74ba39baa453cc4520b158bf1 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 31 Oct 2018 23:23:03 +0100 Subject: [PATCH 103/140] Revert "feat(applets): integrate into toolkit" This reverts commit 7bbe9bb3b352904044a4141304229c1205fb5016. --- .../@aws-cdk/applet-js/bin/cdk-applet-js.ts | 96 +- .../@aws-cdk/applet-js/lib/applet-helpers.ts | 52 - packages/@aws-cdk/applet-js/package-lock.json | 5503 +---------------- packages/@aws-cdk/applet-js/package.json | 2 +- .../@aws-cdk/applet-js/test/expected1.json | 66 +- .../@aws-cdk/applet-js/test/expected2.json | 88 +- .../@aws-cdk/applet-js/test/expected3.json | 16 +- .../applet-js/test/manual-test-npm.yaml | 1 - .../applet-js/test/strip-stacktrace.ts | 26 + .../@aws-cdk/applet-js/test/test-applet.ts | 14 +- .../@aws-cdk/applet-js/test/test-fromnpm.yaml | 1 - .../test/test-multistack-expected.json | 35 - .../applet-js/test/test-multistack.yaml | 9 - .../test/test-nonstack-expected.json | 22 - .../applet-js/test/test-nonstack.yaml | 4 - .../@aws-cdk/applet-js/test/test.applets.ts | 172 - .../@aws-cdk/applet-js/test/test.helpers.ts | 56 - packages/aws-cdk/bin/cdk.ts | 144 +- packages/aws-cdk/lib/exec.ts | 154 - packages/aws-cdk/lib/settings.ts | 3 - packages/aws-cdk/package.json | 1 - 21 files changed, 339 insertions(+), 6126 deletions(-) delete mode 100644 packages/@aws-cdk/applet-js/lib/applet-helpers.ts delete mode 100644 packages/@aws-cdk/applet-js/test/manual-test-npm.yaml create mode 100644 packages/@aws-cdk/applet-js/test/strip-stacktrace.ts delete mode 100644 packages/@aws-cdk/applet-js/test/test-fromnpm.yaml delete mode 100644 packages/@aws-cdk/applet-js/test/test-multistack-expected.json delete mode 100644 packages/@aws-cdk/applet-js/test/test-multistack.yaml delete mode 100644 packages/@aws-cdk/applet-js/test/test-nonstack-expected.json delete mode 100644 packages/@aws-cdk/applet-js/test/test-nonstack.yaml delete mode 100644 packages/@aws-cdk/applet-js/test/test.applets.ts delete mode 100644 packages/@aws-cdk/applet-js/test/test.helpers.ts delete mode 100644 packages/aws-cdk/lib/exec.ts diff --git a/packages/@aws-cdk/applet-js/bin/cdk-applet-js.ts b/packages/@aws-cdk/applet-js/bin/cdk-applet-js.ts index c5d2a86fcd469..98a0fe4efdabe 100644 --- a/packages/@aws-cdk/applet-js/bin/cdk-applet-js.ts +++ b/packages/@aws-cdk/applet-js/bin/cdk-applet-js.ts @@ -2,11 +2,8 @@ import 'source-map-support/register'; import cdk = require('@aws-cdk/cdk'); -import child_process = require('child_process'); import fs = require('fs-extra'); -import os = require('os'); import path = require('path'); -import { isStackConstructor, parseApplet } from '../lib/applet-helpers'; // tslint:disable-next-line:no-var-requires const YAML = require('js-yaml'); @@ -22,84 +19,65 @@ async function main() { const appletFile = process.argv[2]; if (!appletFile) { - throw new Error(`Usage: ${progname} `); + throw new Error(`Usage: ${progname}| `); } - // read applet(s) properties from the provided file - let fileContents = YAML.safeLoad(await fs.readFile(appletFile, { encoding: 'utf-8' })); - if (!Array.isArray(fileContents)) { - fileContents = [fileContents]; - } - - const searchDir = path.dirname(appletFile); - const tempDir = await fs.mkdtemp(os.tmpdir()); - try { - - const app = new cdk.App(); - for (const props of fileContents) { - await constructStack(app, searchDir, tempDir, props); - } - app.run(); - } finally { - await fs.remove(tempDir); - } -} + // read applet properties from the provided file + const props = YAML.safeLoad(await fs.readFile(appletFile, { encoding: 'utf-8' })); -/** - * Construct a stack from the given props - * @param props Const - */ -async function constructStack(app: cdk.App, searchDir: string, tempDir: string, props: any) { // the 'applet' attribute tells us how to load the applet. in the javascript case // it will be in the format : where is technically passed to "require" // and is expected to be exported from the module. - const appletSpec: string = props.applet; - if (!appletSpec) { + const applet: string = props.applet; + if (!applet) { throw new Error('Applet file missing "applet" attribute'); } - const applet = parseApplet(appletSpec); + const { moduleName, className } = parseApplet(applet); // remove the 'applet' attribute as we pass it along to the applet class. delete props.applet; - if (applet.npmPackage) { - // tslint:disable-next-line:no-console - console.error(`Installing NPM package ${applet.npmPackage}`); - // Magic marker to download this package directly off of NPM - // We're going to run NPM as a shell (since programmatic usage is not stable - // by their own admission) and we're installing into a temporary directory. - // (Installing into a permanent directory is useless since NPM doesn't do - // any real caching anyway). - child_process.execFileSync('npm', ['install', '--prefix', tempDir, '--global', applet.npmPackage], { - stdio: 'inherit' - }); - searchDir = path.join(tempDir, 'lib'); - } - // we need to resolve the module name relatively to where the applet file is // and not relative to this module or cwd. - const resolve = require.resolve as any; // escape type-checking since { paths } is not defined - const modulePath = resolve(applet.moduleName, { paths: [ searchDir ] }); + const resolve = require.resolve as any; // escapse type-checking since { paths } is not defined + const modulePath = resolve(moduleName, { paths: [ path.dirname(appletFile) ] }); // load the module const pkg = require(modulePath); // find the applet class within the package // tslint:disable-next-line:variable-name - const appletConstructor = pkg[applet.className]; - if (!appletConstructor) { - throw new Error(`Cannot find applet class "${applet.className}" in module "${applet.moduleName}"`); + const AppletStack = pkg[className]; + if (!AppletStack) { + throw new Error(`Cannot find applet class "${className}" in module "${moduleName}"`); } - const stackName = props.name || applet.className; + // create the CDK app + const app = new cdk.App(); + + const constructName = props.name || className; + + // add the applet stack into the app. + new AppletStack(app, constructName, props); - if (isStackConstructor(appletConstructor)) { - // add the applet stack into the app. - new appletConstructor(app, stackName, props); - } else { - // Make a stack THEN add it in - const stack = new cdk.Stack(app, stackName, props); - new appletConstructor(stack, 'Default', props); + // transfer control to the app + app.run(); +} + +function parseApplet(applet: string) { + const components = applet.split(':'); + // tslint:disable-next-line:prefer-const + let [ moduleName, className ] = components; + + if (components.length > 2 || !moduleName) { + throw new Error(`"applet" value is "${applet}" but it must be in the form "[:]". + If is not specified, "Applet" is the default`); } -} \ No newline at end of file + + if (!className) { + className = 'Applet'; + } + + return { moduleName, className }; +} diff --git a/packages/@aws-cdk/applet-js/lib/applet-helpers.ts b/packages/@aws-cdk/applet-js/lib/applet-helpers.ts deleted file mode 100644 index 2bc519d12ad06..0000000000000 --- a/packages/@aws-cdk/applet-js/lib/applet-helpers.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Determine whether this constructorFunction is going to create an object that inherits from Stack - * - * We do structural typing. - */ -export function isStackConstructor(constructorFn: any) { - // Test for a public method that Stack has - return constructorFn.prototype.findResource !== undefined; -} - -/** - * Extract module name from a NPM package specification - */ -export function extractModuleName(packageSpec: string) { - const m = /^((?:@[a-zA-Z-]+\/)?[a-zA-Z-]+)/i.exec(packageSpec); - if (!m) { throw new Error(`Could not find package name in ${packageSpec}`); } - return m[1]; -} - -export function parseApplet(applet: string): AppletSpec { - const m = /^(npm:\/\/)?([a-z0-9_@./-]+)(:[a-z_0-9]+)?$/i.exec(applet); - if (!m) { - throw new Error(`"applet" value is "${applet}" but it must be in the form "[npm://][:]". - If is not specified, "Applet" is the default`); - } - - if (m[1] === 'npm://') { - return { - npmPackage: m[2], - moduleName: extractModuleName(m[2]), - className: className(m[3]), - }; - } else { - return { - moduleName: m[2], - className: className(m[3]), - }; - } - - function className(s: string | undefined) { - if (s) { - return s.substr(1); - } - return 'Applet'; - } -} - -export interface AppletSpec { - npmPackage?: string; - moduleName: string; - className: string; -} \ No newline at end of file diff --git a/packages/@aws-cdk/applet-js/package-lock.json b/packages/@aws-cdk/applet-js/package-lock.json index f5a43d3c75c91..5fbbaff225f51 100644 --- a/packages/@aws-cdk/applet-js/package-lock.json +++ b/packages/@aws-cdk/applet-js/package-lock.json @@ -1,43 +1,15 @@ { "name": "@aws-cdk/applet-js", - "version": "0.14.1", + "version": "0.12.0", "lockfileVersion": 1, "requires": true, "dependencies": { - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, - "requires": { - "@babel/highlight": "7.0.0" - } - }, - "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, - "requires": { - "chalk": "2.4.1", - "esutils": "2.0.2", - "js-tokens": "4.0.0" - }, - "dependencies": { - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - } - } - }, "@types/fs-extra": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.4.tgz", "integrity": "sha512-DsknoBvD8s+RFfSGjmERJ7ZOP1HI0UZRA3FSI+Zakhrc/Gy26YQsLI+m5V5DHxroHRJqCDLKJp7Hixn8zyaF7g==", "requires": { - "@types/node": "10.12.0" + "@types/node": "*" } }, "@types/js-yaml": { @@ -56,867 +28,12 @@ "integrity": "sha1-0DTh0ynkbo0Pc3yajbl/aPgbU4I=", "dev": true }, - "abab": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", - "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", - "dev": true - }, - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", - "dev": true - }, - "acorn-globals": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz", - "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==", - "dev": true, - "requires": { - "acorn": "6.0.2", - "acorn-walk": "6.1.0" - }, - "dependencies": { - "acorn": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.2.tgz", - "integrity": "sha512-GXmKIvbrN3TV7aVqAzVFaMW8F8wzVX7voEBRO3bDA64+EX37YSayggRJP5Xig6HYHBkWKpFg9W5gg6orklubhg==", - "dev": true - } - } - }, - "acorn-walk": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.0.tgz", - "integrity": "sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg==", - "dev": true - }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, - "ansi-escapes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "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==", - "dev": true, - "requires": { - "color-convert": "1.9.3" - } - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "3.1.10", - "normalize-path": "2.1.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": 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.3", - "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", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": 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", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": 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", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": 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", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "1.0.2" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": 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", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": 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", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": 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.13", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - } - } - } - }, - "append-transform": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", - "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", - "dev": true, - "requires": { - "default-require-extensions": "1.0.0" - } - }, "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" - } - }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "1.1.0" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "dev": true, - "requires": { - "lodash": "4.17.11" - } - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "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=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": 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" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "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=", - "dev": true - } - } - }, - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "dev": true, - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.6.0", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.11", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": 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.11", - "source-map": "0.5.7", - "trim-right": "1.0.1" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-jest": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.6.0.tgz", - "integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==", - "dev": true, - "requires": { - "babel-plugin-istanbul": "4.1.6", - "babel-preset-jest": "23.2.0" - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-istanbul": { - "version": "4.1.6", - "resolved": "http://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz", - "integrity": "sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==", - "dev": true, - "requires": { - "babel-plugin-syntax-object-rest-spread": "6.13.0", - "find-up": "2.1.0", - "istanbul-lib-instrument": "1.10.2", - "test-exclude": "4.2.3" - } - }, - "babel-plugin-jest-hoist": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz", - "integrity": "sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc=", - "dev": true - }, - "babel-plugin-syntax-object-rest-spread": { - "version": "6.13.0", - "resolved": "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", - "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", - "dev": true - }, - "babel-preset-jest": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz", - "integrity": "sha1-jsegOhOPABoaj7HoETZSvxpV2kY=", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "23.2.0", - "babel-plugin-syntax-object-rest-spread": "6.13.0" - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "dev": true, - "requires": { - "babel-core": "6.26.3", - "babel-runtime": "6.26.0", - "core-js": "2.5.7", - "home-or-tmp": "2.0.0", - "lodash": "4.17.11", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "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==", - "dev": true, - "requires": { - "source-map": "0.5.7" - } - } - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "2.5.7", - "regenerator-runtime": "0.11.1" - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "lodash": "4.17.11" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": 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.11" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.11", - "to-fast-properties": "1.0.3" - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": 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", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "1.0.2" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "1.8.2", - "preserve": "0.2.0", - "repeat-element": "1.1.3" - } - }, - "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", - "dev": true - }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "dev": true, - "requires": { - "resolve": "1.1.7" - } - }, - "bser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", - "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", - "dev": true, - "requires": { - "node-int64": "0.4.0" + "sprintf-js": "~1.0.2" } }, "buffer-from": { @@ -924,4602 +41,66 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": 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", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "capture-exit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-1.2.0.tgz", - "integrity": "sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28=", - "dev": true, - "requires": { - "rsvp": "3.6.2" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.5.0" - } - }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": 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", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "0.1.6" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "wrap-ansi": "2.1.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "1.0.0", - "object-visit": "1.0.1" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "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=", - "dev": true - }, - "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "dev": true, - "optional": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-js": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==", - "dev": true - }, - "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=", - "dev": true - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "4.1.3", - "shebang-command": "1.2.0", - "which": "1.3.1" - } - }, - "cssom": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.4.tgz", - "integrity": "sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog==", - "dev": true - }, - "cssstyle": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.1.1.tgz", - "integrity": "sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog==", - "dev": true, - "requires": { - "cssom": "0.3.4" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "1.0.0" - } - }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "dev": true, - "requires": { - "abab": "2.0.0", - "whatwg-mimetype": "2.2.0", - "whatwg-url": "7.0.0" - }, - "dependencies": { - "whatwg-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", - "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", - "dev": true, - "requires": { - "lodash.sortby": "4.7.0", - "tr46": "1.0.1", - "webidl-conversions": "4.0.2" - } - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, - "default-require-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", - "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", - "dev": true, + "fs-extra": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.0.tgz", + "integrity": "sha512-EglNDLRpmaTWiD/qraZn6HREAEAHJcJOmxNEYwq6xeMKnVMAy3GUcFB+wXt2C6k4CNvB/mP1y/U3dzvKKj5OtQ==", "requires": { - "strip-bom": "2.0.0" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" } }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "1.0.12" - } + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", "requires": { - "is-descriptor": "1.0.2", - "isobject": "3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } + "argparse": "^1.0.7", + "esprima": "^4.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=", - "dev": true - }, - "detect-indent": { + "jsonfile": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "requires": { - "repeating": "2.0.1" + "graceful-fs": "^4.1.6" } }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", - "dev": true - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "dev": true, + "source-map-support": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", + "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", "requires": { - "webidl-conversions": "4.0.2" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "0.1.1", - "safer-buffer": "2.1.2" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "0.2.1" - } - }, - "es-abstract": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", - "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", - "dev": true, - "requires": { - "es-to-primitive": "1.2.0", - "function-bind": "1.1.1", - "has": "1.0.3", - "is-callable": "1.1.4", - "is-regex": "1.0.4" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "requires": { - "is-callable": "1.1.4", - "is-date-object": "1.0.1", - "is-symbol": "1.0.2" - } - }, - "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=", - "dev": true - }, - "escodegen": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz", - "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==", - "dev": true, - "requires": { - "esprima": "3.1.3", - "estraverse": "4.2.0", - "esutils": "2.0.2", - "optionator": "0.8.2", - "source-map": "0.6.1" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "exec-sh": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz", - "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==", - "dev": true, - "requires": { - "merge": "1.2.1" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": 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" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "0.1.1" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "2.2.4" - } - }, - "expect": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-23.6.0.tgz", - "integrity": "sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w==", - "dev": true, - "requires": { - "ansi-styles": "3.2.1", - "jest-diff": "23.6.0", - "jest-get-type": "22.4.3", - "jest-matcher-utils": "23.6.0", - "jest-message-util": "23.4.0", - "jest-regex-util": "23.3.0" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "2.0.4" - } - } - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "1.0.0" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", - "dev": true, - "requires": { - "bser": "2.0.0" - } - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, - "fileset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", - "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", - "dev": true, - "requires": { - "glob": "7.1.3", - "minimatch": "3.0.4" - } - }, - "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "dev": true, - "requires": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "3.1.1", - "repeat-element": "1.1.3", - "repeat-string": "1.6.1" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "2.0.0" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "1.0.2" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.7", - "mime-types": "2.1.21" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "0.2.2" - } - }, - "fs-extra": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.0.tgz", - "integrity": "sha512-EglNDLRpmaTWiD/qraZn6HREAEAHJcJOmxNEYwq6xeMKnVMAy3GUcFB+wXt2C6k4CNvB/mP1y/U3dzvKKj5OtQ==", - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "4.0.0", - "universalify": "0.1.2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.11.1", - "node-pre-gyp": "0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": 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" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.21", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.7", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.5.1", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "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.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.4", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "1.0.0" - } - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": 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" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true, - "requires": { - "glob-parent": "2.0.0", - "is-glob": "2.0.1" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "2.0.1" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true - }, - "handlebars": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz", - "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==", - "dev": true, - "requires": { - "async": "2.6.1", - "optimist": "0.6.1", - "source-map": "0.6.1", - "uglify-js": "3.4.9" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", - "dev": true, - "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "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=", - "dev": true - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "2.0.6", - "has-values": "1.0.0", - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "dev": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", - "dev": true - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "1.0.5" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.15.1" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "import-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", - "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", - "dev": true, - "requires": { - "pkg-dir": "2.0.0", - "resolve-cwd": "2.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "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=", - "dev": true - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "1.4.0" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true, - "requires": { - "builtin-modules": "1.1.1" - } - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true - }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "dev": true, - "requires": { - "ci-info": "1.6.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": 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", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "dev": true, - "requires": { - "is-primitive": "2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-generator-fn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-1.0.0.tgz", - "integrity": "sha1-lp1J4bszKfa7fwkIm+JleLLd1Go=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "1.0.0" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "1.0.3" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, - "requires": { - "has-symbols": "1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul-api": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz", - "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", - "dev": true, - "requires": { - "async": "2.6.1", - "fileset": "2.0.3", - "istanbul-lib-coverage": "1.2.1", - "istanbul-lib-hook": "1.2.2", - "istanbul-lib-instrument": "1.10.2", - "istanbul-lib-report": "1.1.5", - "istanbul-lib-source-maps": "1.2.6", - "istanbul-reports": "1.5.1", - "js-yaml": "3.12.0", - "mkdirp": "0.5.1", - "once": "1.4.0" - } - }, - "istanbul-lib-coverage": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", - "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz", - "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", - "dev": true, - "requires": { - "append-transform": "0.4.0" - } - }, - "istanbul-lib-instrument": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", - "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", - "dev": 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.1", - "semver": "5.6.0" - } - }, - "istanbul-lib-report": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz", - "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "1.2.1", - "mkdirp": "0.5.1", - "path-parse": "1.0.6", - "supports-color": "3.2.3" - }, - "dependencies": { - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "1.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz", - "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", - "dev": true, - "requires": { - "debug": "3.2.6", - "istanbul-lib-coverage": "1.2.1", - "mkdirp": "0.5.1", - "rimraf": "2.6.2", - "source-map": "0.5.7" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "2.1.1" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz", - "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", - "dev": true, - "requires": { - "handlebars": "4.0.12" - } - }, - "jest": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-23.6.0.tgz", - "integrity": "sha512-lWzcd+HSiqeuxyhG+EnZds6iO3Y3ZEnMrfZq/OTGvF/C+Z4fPMCdhWTGSAiO2Oym9rbEXfwddHhh6jqrTF3+Lw==", - "dev": true, - "requires": { - "import-local": "1.0.0", - "jest-cli": "23.6.0" - }, - "dependencies": { - "jest-cli": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.6.0.tgz", - "integrity": "sha512-hgeD1zRUp1E1zsiyOXjEn4LzRLWdJBV//ukAHGlx6s5mfCNJTbhbHjgxnDUXA8fsKWN/HqFFF6X5XcCwC/IvYQ==", - "dev": true, - "requires": { - "ansi-escapes": "3.1.0", - "chalk": "2.4.1", - "exit": "0.1.2", - "glob": "7.1.3", - "graceful-fs": "4.1.11", - "import-local": "1.0.0", - "is-ci": "1.2.1", - "istanbul-api": "1.3.7", - "istanbul-lib-coverage": "1.2.1", - "istanbul-lib-instrument": "1.10.2", - "istanbul-lib-source-maps": "1.2.6", - "jest-changed-files": "23.4.2", - "jest-config": "23.6.0", - "jest-environment-jsdom": "23.4.0", - "jest-get-type": "22.4.3", - "jest-haste-map": "23.6.0", - "jest-message-util": "23.4.0", - "jest-regex-util": "23.3.0", - "jest-resolve-dependencies": "23.6.0", - "jest-runner": "23.6.0", - "jest-runtime": "23.6.0", - "jest-snapshot": "23.6.0", - "jest-util": "23.4.0", - "jest-validate": "23.6.0", - "jest-watcher": "23.4.0", - "jest-worker": "23.2.0", - "micromatch": "2.3.11", - "node-notifier": "5.3.0", - "prompts": "0.1.14", - "realpath-native": "1.0.2", - "rimraf": "2.6.2", - "slash": "1.0.0", - "string-length": "2.0.0", - "strip-ansi": "4.0.0", - "which": "1.3.1", - "yargs": "11.1.0" - } - }, - "yargs": { - "version": "11.1.0", - "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", - "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", - "dev": true, - "requires": { - "cliui": "4.1.0", - "decamelize": "1.2.0", - "find-up": "2.1.0", - "get-caller-file": "1.0.3", - "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" - } - } - } - }, - "jest-changed-files": { - "version": "23.4.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-23.4.2.tgz", - "integrity": "sha512-EyNhTAUWEfwnK0Is/09LxoqNDOn7mU7S3EHskG52djOFS/z+IT0jT3h3Ql61+dklcG7bJJitIWEMB4Sp1piHmA==", - "dev": true, - "requires": { - "throat": "4.1.0" - } - }, - "jest-config": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-23.6.0.tgz", - "integrity": "sha512-i8V7z9BeDXab1+VNo78WM0AtWpBRXJLnkT+lyT+Slx/cbP5sZJ0+NDuLcmBE5hXAoK0aUp7vI+MOxR+R4d8SRQ==", - "dev": true, - "requires": { - "babel-core": "6.26.3", - "babel-jest": "23.6.0", - "chalk": "2.4.1", - "glob": "7.1.3", - "jest-environment-jsdom": "23.4.0", - "jest-environment-node": "23.4.0", - "jest-get-type": "22.4.3", - "jest-jasmine2": "23.6.0", - "jest-regex-util": "23.3.0", - "jest-resolve": "23.6.0", - "jest-util": "23.4.0", - "jest-validate": "23.6.0", - "micromatch": "2.3.11", - "pretty-format": "23.6.0" - } - }, - "jest-diff": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.6.0.tgz", - "integrity": "sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==", - "dev": true, - "requires": { - "chalk": "2.4.1", - "diff": "3.5.0", - "jest-get-type": "22.4.3", - "pretty-format": "23.6.0" - } - }, - "jest-docblock": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-23.2.0.tgz", - "integrity": "sha1-8IXh8YVI2Z/dabICB+b9VdkTg6c=", - "dev": true, - "requires": { - "detect-newline": "2.1.0" - } - }, - "jest-each": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-23.6.0.tgz", - "integrity": "sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg==", - "dev": true, - "requires": { - "chalk": "2.4.1", - "pretty-format": "23.6.0" - } - }, - "jest-environment-jsdom": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-23.4.0.tgz", - "integrity": "sha1-BWp5UrP+pROsYqFAosNox52eYCM=", - "dev": true, - "requires": { - "jest-mock": "23.2.0", - "jest-util": "23.4.0", - "jsdom": "11.12.0" - } - }, - "jest-environment-node": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-23.4.0.tgz", - "integrity": "sha1-V+gO0IQd6jAxZ8zozXlSHeuv3hA=", - "dev": true, - "requires": { - "jest-mock": "23.2.0", - "jest-util": "23.4.0" - } - }, - "jest-get-type": { - "version": "22.4.3", - "resolved": "http://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", - "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", - "dev": true - }, - "jest-haste-map": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-23.6.0.tgz", - "integrity": "sha512-uyNhMyl6dr6HaXGHp8VF7cK6KpC6G9z9LiMNsst+rJIZ8l7wY0tk8qwjPmEghczojZ2/ZhtEdIabZ0OQRJSGGg==", - "dev": true, - "requires": { - "fb-watchman": "2.0.0", - "graceful-fs": "4.1.11", - "invariant": "2.2.4", - "jest-docblock": "23.2.0", - "jest-serializer": "23.0.1", - "jest-worker": "23.2.0", - "micromatch": "2.3.11", - "sane": "2.5.2" - } - }, - "jest-jasmine2": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-23.6.0.tgz", - "integrity": "sha512-pe2Ytgs1nyCs8IvsEJRiRTPC0eVYd8L/dXJGU08GFuBwZ4sYH/lmFDdOL3ZmvJR8QKqV9MFuwlsAi/EWkFUbsQ==", - "dev": true, - "requires": { - "babel-traverse": "6.26.0", - "chalk": "2.4.1", - "co": "4.6.0", - "expect": "23.6.0", - "is-generator-fn": "1.0.0", - "jest-diff": "23.6.0", - "jest-each": "23.6.0", - "jest-matcher-utils": "23.6.0", - "jest-message-util": "23.4.0", - "jest-snapshot": "23.6.0", - "jest-util": "23.4.0", - "pretty-format": "23.6.0" - } - }, - "jest-leak-detector": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz", - "integrity": "sha512-f/8zA04rsl1Nzj10HIyEsXvYlMpMPcy0QkQilVZDFOaPbv2ur71X5u2+C4ZQJGyV/xvVXtCCZ3wQ99IgQxftCg==", - "dev": true, - "requires": { - "pretty-format": "23.6.0" - } - }, - "jest-matcher-utils": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz", - "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", - "dev": true, - "requires": { - "chalk": "2.4.1", - "jest-get-type": "22.4.3", - "pretty-format": "23.6.0" - } - }, - "jest-message-util": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.4.0.tgz", - "integrity": "sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8=", - "dev": true, - "requires": { - "@babel/code-frame": "7.0.0", - "chalk": "2.4.1", - "micromatch": "2.3.11", - "slash": "1.0.0", - "stack-utils": "1.0.1" - } - }, - "jest-mock": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-23.2.0.tgz", - "integrity": "sha1-rRxg8p6HGdR8JuETgJi20YsmETQ=", - "dev": true - }, - "jest-regex-util": { - "version": "23.3.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.3.0.tgz", - "integrity": "sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U=", - "dev": true - }, - "jest-resolve": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-23.6.0.tgz", - "integrity": "sha512-XyoRxNtO7YGpQDmtQCmZjum1MljDqUCob7XlZ6jy9gsMugHdN2hY4+Acz9Qvjz2mSsOnPSH7skBmDYCHXVZqkA==", - "dev": true, - "requires": { - "browser-resolve": "1.11.3", - "chalk": "2.4.1", - "realpath-native": "1.0.2" - } - }, - "jest-resolve-dependencies": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-23.6.0.tgz", - "integrity": "sha512-EkQWkFWjGKwRtRyIwRwI6rtPAEyPWlUC2MpzHissYnzJeHcyCn1Hc8j7Nn1xUVrS5C6W5+ZL37XTem4D4pLZdA==", - "dev": true, - "requires": { - "jest-regex-util": "23.3.0", - "jest-snapshot": "23.6.0" - } - }, - "jest-runner": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-23.6.0.tgz", - "integrity": "sha512-kw0+uj710dzSJKU6ygri851CObtCD9cN8aNkg8jWJf4ewFyEa6kwmiH/r/M1Ec5IL/6VFa0wnAk6w+gzUtjJzA==", - "dev": true, - "requires": { - "exit": "0.1.2", - "graceful-fs": "4.1.11", - "jest-config": "23.6.0", - "jest-docblock": "23.2.0", - "jest-haste-map": "23.6.0", - "jest-jasmine2": "23.6.0", - "jest-leak-detector": "23.6.0", - "jest-message-util": "23.4.0", - "jest-runtime": "23.6.0", - "jest-util": "23.4.0", - "jest-worker": "23.2.0", - "source-map-support": "0.5.9", - "throat": "4.1.0" - } - }, - "jest-runtime": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-23.6.0.tgz", - "integrity": "sha512-ycnLTNPT2Gv+TRhnAYAQ0B3SryEXhhRj1kA6hBPSeZaNQkJ7GbZsxOLUkwg6YmvWGdX3BB3PYKFLDQCAE1zNOw==", - "dev": true, - "requires": { - "babel-core": "6.26.3", - "babel-plugin-istanbul": "4.1.6", - "chalk": "2.4.1", - "convert-source-map": "1.6.0", - "exit": "0.1.2", - "fast-json-stable-stringify": "2.0.0", - "graceful-fs": "4.1.11", - "jest-config": "23.6.0", - "jest-haste-map": "23.6.0", - "jest-message-util": "23.4.0", - "jest-regex-util": "23.3.0", - "jest-resolve": "23.6.0", - "jest-snapshot": "23.6.0", - "jest-util": "23.4.0", - "jest-validate": "23.6.0", - "micromatch": "2.3.11", - "realpath-native": "1.0.2", - "slash": "1.0.0", - "strip-bom": "3.0.0", - "write-file-atomic": "2.3.0", - "yargs": "11.1.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "yargs": { - "version": "11.1.0", - "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", - "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", - "dev": true, - "requires": { - "cliui": "4.1.0", - "decamelize": "1.2.0", - "find-up": "2.1.0", - "get-caller-file": "1.0.3", - "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" - } - } - } - }, - "jest-serializer": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-23.0.1.tgz", - "integrity": "sha1-o3dq6zEekP6D+rnlM+hRAr0WQWU=", - "dev": true - }, - "jest-snapshot": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-23.6.0.tgz", - "integrity": "sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg==", - "dev": true, - "requires": { - "babel-types": "6.26.0", - "chalk": "2.4.1", - "jest-diff": "23.6.0", - "jest-matcher-utils": "23.6.0", - "jest-message-util": "23.4.0", - "jest-resolve": "23.6.0", - "mkdirp": "0.5.1", - "natural-compare": "1.4.0", - "pretty-format": "23.6.0", - "semver": "5.6.0" - } - }, - "jest-util": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-23.4.0.tgz", - "integrity": "sha1-TQY8uSe68KI4Mf9hvsLLv0l5NWE=", - "dev": true, - "requires": { - "callsites": "2.0.0", - "chalk": "2.4.1", - "graceful-fs": "4.1.11", - "is-ci": "1.2.1", - "jest-message-util": "23.4.0", - "mkdirp": "0.5.1", - "slash": "1.0.0", - "source-map": "0.6.1" - } - }, - "jest-validate": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz", - "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", - "dev": true, - "requires": { - "chalk": "2.4.1", - "jest-get-type": "22.4.3", - "leven": "2.1.0", - "pretty-format": "23.6.0" - } - }, - "jest-watcher": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-23.4.0.tgz", - "integrity": "sha1-0uKM50+NrWxq/JIrksq+9u0FyRw=", - "dev": true, - "requires": { - "ansi-escapes": "3.1.0", - "chalk": "2.4.1", - "string-length": "2.0.0" - } - }, - "jest-worker": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-23.2.0.tgz", - "integrity": "sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk=", - "dev": true, - "requires": { - "merge-stream": "1.0.1" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", - "requires": { - "argparse": "1.0.10", - "esprima": "4.0.1" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "jsdom": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", - "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", - "dev": true, - "requires": { - "abab": "2.0.0", - "acorn": "5.7.3", - "acorn-globals": "4.3.0", - "array-equal": "1.0.0", - "cssom": "0.3.4", - "cssstyle": "1.1.1", - "data-urls": "1.1.0", - "domexception": "1.0.1", - "escodegen": "1.11.0", - "html-encoding-sniffer": "1.0.2", - "left-pad": "1.3.0", - "nwsapi": "2.0.9", - "parse5": "4.0.0", - "pn": "1.1.0", - "request": "2.88.0", - "request-promise-native": "1.0.5", - "sax": "1.2.4", - "symbol-tree": "3.2.2", - "tough-cookie": "2.4.3", - "w3c-hr-time": "1.0.1", - "webidl-conversions": "4.0.2", - "whatwg-encoding": "1.0.5", - "whatwg-mimetype": "2.2.0", - "whatwg-url": "6.5.0", - "ws": "5.2.2", - "xml-name-validator": "3.0.0" - } - }, - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - }, - "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=", - "dev": true - }, - "json5": { - "version": "0.5.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "4.1.11" - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - }, - "kleur": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-2.0.2.tgz", - "integrity": "sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ==", - "dev": true - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "1.0.0" - } - }, - "left-pad": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", - "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", - "dev": true - }, - "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": 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", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "2.0.0", - "path-exists": "3.0.0" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "3.0.2" - } - }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "dev": true, - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" - } - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "requires": { - "tmpl": "1.0.4" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "1.0.1" - } - }, - "math-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", - "dev": true - }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "dev": true, - "requires": { - "mimic-fn": "1.2.0" - } - }, - "merge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", - "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", - "dev": true - }, - "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "dev": true, - "requires": { - "readable-stream": "2.3.6" - } - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "2.0.0", - "array-unique": "0.2.1", - "braces": "1.8.5", - "expand-brackets": "0.1.5", - "extglob": "0.3.2", - "filename-regex": "2.0.1", - "is-extglob": "1.0.0", - "is-glob": "2.0.1", - "kind-of": "3.2.2", - "normalize-path": "2.1.1", - "object.omit": "2.0.1", - "parse-glob": "3.0.4", - "regex-cache": "0.4.4" - } - }, - "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", - "dev": true - }, - "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", - "dev": true, - "requires": { - "mime-db": "1.37.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "dev": true, - "requires": { - "for-in": "1.0.2", - "is-extendable": "1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "nan": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", - "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": 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-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", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-notifier": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.3.0.tgz", - "integrity": "sha512-AhENzCSGZnZJgBARsUjnQ7DnZbzyP+HxlVXuD0xqAnvL8q+OqtSX7lGg9e8nHzwXkMMXNdVeqq4E2M3EUAqX6Q==", - "dev": true, - "requires": { - "growly": "1.3.0", - "semver": "5.6.0", - "shellwords": "0.1.1", - "which": "1.3.1" - } - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "dev": true, - "requires": { - "hosted-git-info": "2.7.1", - "is-builtin-module": "1.0.0", - "semver": "5.6.0", - "validate-npm-package-license": "3.0.4" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "1.1.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "2.0.1" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "nwsapi": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.9.tgz", - "integrity": "sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ==", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "0.1.1", - "define-property": "0.2.5", - "kind-of": "3.2.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "0.1.6" - } - } - } - }, - "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, - "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.12.0" - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true, - "requires": { - "for-own": "0.1.5", - "is-extendable": "0.1.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "0.0.8", - "wordwrap": "0.0.3" - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "2.0.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "1.0.0" - }, - "dependencies": { - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - } - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", - "dev": true, - "requires": { - "execa": "0.7.0", - "lcid": "1.0.0", - "mem": "1.1.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "1.3.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true, - "requires": { - "glob-base": "0.3.0", - "is-dotfile": "1.0.3", - "is-extglob": "1.0.0", - "is-glob": "2.0.1" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "1.3.2" - } - }, - "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "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=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "2.0.4" - } - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "2.1.0" - } - }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", - "dev": true - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, - "pretty-format": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", - "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", - "dev": true, - "requires": { - "ansi-regex": "3.0.0", - "ansi-styles": "3.2.1" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - } - } - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "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==", - "dev": true - }, - "prompts": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-0.1.14.tgz", - "integrity": "sha512-rxkyiE9YH6zAz/rZpywySLKkpaj0NMVyNw1qhsubdbjjSgcayjTShDreZGlFMcGSu5sab3bAKPfFk78PB90+8w==", - "dev": true, - "requires": { - "kleur": "2.0.2", - "sisteransi": "0.1.1" - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "psl": { - "version": "1.1.29", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", - "dev": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "randomatic": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", - "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", - "dev": true, - "requires": { - "is-number": "4.0.0", - "kind-of": "6.0.2", - "math-random": "1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": 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", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "2.0.1" - } - } - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "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" - } - }, - "realpath-native": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.2.tgz", - "integrity": "sha512-+S3zTvVt9yTntFrBpm7TQmQ3tzpCrnA1a/y+3cUHAc9ZR6aIjG0WNLR+Rj79QpJktY+VeW/TQtFlQ1bzsehI8g==", - "dev": true, - "requires": { - "util.promisify": "1.0.0" - } - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "dev": true, - "requires": { - "is-equal-shallow": "0.1.3" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "3.0.2", - "safe-regex": "1.1.0" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "1.0.2" - } - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.8.0", - "caseless": "0.12.0", - "combined-stream": "1.0.7", - "extend": "3.0.2", - "forever-agent": "0.6.1", - "form-data": "2.3.3", - "har-validator": "5.1.0", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.21", - "oauth-sign": "0.9.0", - "performance-now": "2.1.0", - "qs": "6.5.2", - "safe-buffer": "5.1.2", - "tough-cookie": "2.4.3", - "tunnel-agent": "0.6.0", - "uuid": "3.3.2" - } - }, - "request-promise-core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", - "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", - "dev": true, - "requires": { - "lodash": "4.17.11" - } - }, - "request-promise-native": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", - "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", - "dev": true, - "requires": { - "request-promise-core": "1.1.1", - "stealthy-require": "1.1.1", - "tough-cookie": "2.4.3" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "requires": { - "glob": "7.1.3" - } - }, - "rsvp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", - "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==", - "dev": true - }, - "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==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "0.1.15" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sane": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/sane/-/sane-2.5.2.tgz", - "integrity": "sha1-tNwYYcIbQn6SlQej51HiosuKs/o=", - "dev": true, - "requires": { - "anymatch": "2.0.0", - "capture-exit": "1.2.0", - "exec-sh": "0.2.2", - "fb-watchman": "2.0.0", - "fsevents": "1.2.4", - "micromatch": "3.1.10", - "minimist": "1.2.0", - "walker": "1.0.7", - "watch": "0.18.0" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": 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.3", - "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", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": 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", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": 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", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": 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", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "1.0.2" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": 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", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": 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", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": 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.13", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "dev": 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", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "sisteransi": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz", - "integrity": "sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g==", - "dev": true - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": 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.2", - "use": "3.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "1.0.0", - "isobject": "3.0.1", - "snapdragon-util": "3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "1.0.2" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "2.1.2", - "decode-uri-component": "0.2.0", - "resolve-url": "0.2.1", - "source-map-url": "0.4.0", - "urix": "0.1.0" - } - }, - "source-map-support": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", - "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", - "requires": { - "buffer-from": "1.1.1", - "source-map": "0.6.1" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "spdx-correct": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.2.tgz", - "integrity": "sha512-q9hedtzyXHr5S0A1vEPoK/7l8NpfkFYTq6iCY+Pno2ZbdZR6WexZFtqeVGkGxW3TEJMN914Z55EnAGMmenlIQQ==", - "dev": true, - "requires": { - "spdx-expression-parse": "3.0.0", - "spdx-license-ids": "3.0.1" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "2.2.0", - "spdx-license-ids": "3.0.1" - } - }, - "spdx-license-ids": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.1.tgz", - "integrity": "sha512-TfOfPcYGBB5sDuPn3deByxPhmfegAhpDYKSOXZQN81Oyrrif8ZCodOLzK3AesELnCx03kikhyDwh0pfvvQvF8w==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "3.0.2" - } - }, - "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.15.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.1.tgz", - "integrity": "sha512-mSdgNUaidk+dRU5MhYtN9zebdzF2iG0cNPWy8HG+W8y+fT1JnSkh0fzzpjOa0L7P8i1Rscz38t0h4gPcKz43xA==", - "dev": true, - "requires": { - "asn1": "0.2.4", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.2", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.2", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "safer-buffer": "2.1.2", - "tweetnacl": "0.14.5" - } - }, - "stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "0.2.5", - "object-copy": "0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "0.1.6" - } - } - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, - "string-length": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", - "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", - "dev": true, - "requires": { - "astral-regex": "1.0.0", - "strip-ansi": "4.0.0" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" - } - }, - "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==", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - } - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "0.2.1" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - }, - "symbol-tree": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", - "dev": true - }, - "test-exclude": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.3.tgz", - "integrity": "sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA==", - "dev": true, - "requires": { - "arrify": "1.0.1", - "micromatch": "2.3.11", - "object-assign": "4.1.1", - "read-pkg-up": "1.0.1", - "require-main-filename": "1.0.1" - } - }, - "throat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", - "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", - "dev": true - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", - "dev": true - }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": 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", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "3.0.0", - "repeat-string": "1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - } - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "1.1.29", - "punycode": "1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "2.1.1" - } - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "1.1.2" - } - }, - "uglify-js": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", - "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", - "dev": true, - "optional": true, - "requires": { - "commander": "2.17.1", - "source-map": "0.6.1" - } - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "dev": 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", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "to-object-path": "0.3.0" - } - } - } + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "0.3.1", - "isobject": "3.0.1" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "2.0.6", - "has-values": "0.1.4", - "isobject": "2.1.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "requires": { - "define-properties": "1.1.3", - "object.getownpropertydescriptors": "2.0.3" - } - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "3.0.2", - "spdx-expression-parse": "3.0.0" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - } - }, - "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", - "dev": true, - "requires": { - "browser-process-hrtime": "0.1.3" - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "dev": true, - "requires": { - "makeerror": "1.0.11" - } - }, - "watch": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", - "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", - "dev": true, - "requires": { - "exec-sh": "0.2.2", - "minimist": "1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - } - }, - "whatwg-mimetype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz", - "integrity": "sha512-5YSO1nMd5D1hY3WzAQV3PzZL83W3YeyR1yW9PcH26Weh1t+Vzh9B6XkDh7aXm83HBZ4nSMvkjvN2H2ySWIvBgw==", - "dev": true - }, - "whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", - "dev": true, - "requires": { - "lodash.sortby": "4.7.0", - "tr46": "1.0.1", - "webidl-conversions": "4.0.2" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write-file-atomic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", - "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "imurmurhash": "0.1.4", - "signal-exit": "3.0.2" - } - }, - "ws": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", - "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", - "dev": true, - "requires": { - "async-limiter": "1.0.0" - } - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, - "yargs-parser": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", - "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", - "dev": true, - "requires": { - "camelcase": "4.1.0" - } } } } diff --git a/packages/@aws-cdk/applet-js/package.json b/packages/@aws-cdk/applet-js/package.json index abb9adcc47967..039e14afe1792 100644 --- a/packages/@aws-cdk/applet-js/package.json +++ b/packages/@aws-cdk/applet-js/package.json @@ -11,7 +11,7 @@ "build": "cdk-build", "watch": "cdk-watch", "lint": "cdk-lint", - "test": "cdk-test && /bin/bash test/test-applet.sh", + "test": "cdk-test", "pkglint": "pkglint -f", "package": "cdk-package" }, diff --git a/packages/@aws-cdk/applet-js/test/expected1.json b/packages/@aws-cdk/applet-js/test/expected1.json index 5f49964fdcf1e..375db10291244 100644 --- a/packages/@aws-cdk/applet-js/test/expected1.json +++ b/packages/@aws-cdk/applet-js/test/expected1.json @@ -1,35 +1,39 @@ { - "name": "TestApplet", - "template": { - "Parameters": { - "P1": { - "Type": "String", - "Default": "hello" + "stacks": [ + { + "name": "TestApplet", + "template": { + "Parameters": { + "P1": { + "Type": "String", + "Default": "hello" + }, + "P2": { + "Type": "Number", + "Default": 123 + } + } }, - "P2": { - "Type": "Number", - "Default": 123 - } - } - }, - "metadata": { - "/TestApplet/P1": [ - { - "type": "aws:cdk:logicalId", - "data": "P1", - "trace": [ - "**REDACTED**" + "metadata": { + "/TestApplet/P1": [ + { + "type": "aws:cdk:logicalId", + "data": "P1", + "trace": [ + "**REDACTED**" + ] + } + ], + "/TestApplet/P2": [ + { + "type": "aws:cdk:logicalId", + "data": "P2", + "trace": [ + "**REDACTED**" + ] + } ] } - ], - "/TestApplet/P2": [ - { - "type": "aws:cdk:logicalId", - "data": "P2", - "trace": [ - "**REDACTED**" - ] - } - ] - } -} + } + ] +} \ No newline at end of file diff --git a/packages/@aws-cdk/applet-js/test/expected2.json b/packages/@aws-cdk/applet-js/test/expected2.json index 3ef09cc69797f..34d1826651cff 100644 --- a/packages/@aws-cdk/applet-js/test/expected2.json +++ b/packages/@aws-cdk/applet-js/test/expected2.json @@ -1,48 +1,52 @@ { - "name": "TestApplet", - "template": { - "Parameters": { - "P1": { - "Type": "String", - "Default": "hello" + "stacks": [ + { + "name": "TestApplet", + "template": { + "Parameters": { + "P1": { + "Type": "String", + "Default": "hello" + }, + "P2": { + "Type": "Number", + "Default": 123 + }, + "P3": { + "Type": "StringList", + "Default": "hello,this,is,awesome,12345" + } + } }, - "P2": { - "Type": "Number", - "Default": 123 - }, - "P3": { - "Type": "StringList", - "Default": "hello,this,is,awesome,12345" - } - } - }, - "metadata": { - "/TestApplet/P1": [ - { - "type": "aws:cdk:logicalId", - "data": "P1", - "trace": [ - "**REDACTED**" + "metadata": { + "/TestApplet/P1": [ + { + "type": "aws:cdk:logicalId", + "data": "P1", + "trace": [ + "**REDACTED**" ] - } - ], - "/TestApplet/P2": [ - { - "type": "aws:cdk:logicalId", - "data": "P2", - "trace": [ - "**REDACTED**" + } + ], + "/TestApplet/P2": [ + { + "type": "aws:cdk:logicalId", + "data": "P2", + "trace": [ + "**REDACTED**" ] - } - ], - "/TestApplet/P3": [ - { - "type": "aws:cdk:logicalId", - "data": "P3", - "trace": [ - "**REDACTED**" + } + ], + "/TestApplet/P3": [ + { + "type": "aws:cdk:logicalId", + "data": "P3", + "trace": [ + "**REDACTED**" ] + } + ] } - ] - } -} + } + ] +} \ No newline at end of file diff --git a/packages/@aws-cdk/applet-js/test/expected3.json b/packages/@aws-cdk/applet-js/test/expected3.json index 495c9527bb181..5de08918673b3 100644 --- a/packages/@aws-cdk/applet-js/test/expected3.json +++ b/packages/@aws-cdk/applet-js/test/expected3.json @@ -1,7 +1,11 @@ { - "name": "Applet", - "template": { - "Description": "this should be reflected in the template description" - }, - "metadata": {} -} + "stacks": [ + { + "name": "Applet", + "template": { + "Description": "this should be reflected in the template description" + }, + "metadata": {} + } + ] +} \ No newline at end of file diff --git a/packages/@aws-cdk/applet-js/test/manual-test-npm.yaml b/packages/@aws-cdk/applet-js/test/manual-test-npm.yaml deleted file mode 100644 index 6d5e422492d40..0000000000000 --- a/packages/@aws-cdk/applet-js/test/manual-test-npm.yaml +++ /dev/null @@ -1 +0,0 @@ -applet: npm://@aws-cdk/aws-ecs:Hello diff --git a/packages/@aws-cdk/applet-js/test/strip-stacktrace.ts b/packages/@aws-cdk/applet-js/test/strip-stacktrace.ts new file mode 100644 index 0000000000000..154b2449bc897 --- /dev/null +++ b/packages/@aws-cdk/applet-js/test/strip-stacktrace.ts @@ -0,0 +1,26 @@ +import 'source-map-support/register'; + +function outputPatchedJson(text: string) { + const document: any = JSON.parse(text); + for (const stack of (document.stacks as any[])) { + for (const key of Object.keys(stack.metadata)) { + if (!stack.metadata[key]) { continue; } + for (const entry of (stack.metadata[key] as any[])) { + if (entry.trace) { entry.trace = ['**REDACTED**']; } + } + } + } + if ('runtime' in document) { + delete document.runtime; + } + process.stdout.write(JSON.stringify(document, null, 2)); +} + +function main() { + let inputText: string = ''; + process.stdin.setEncoding('utf8') + .on('data', chunk => inputText += chunk) + .on('end', () => outputPatchedJson(inputText)); +} + +main(); diff --git a/packages/@aws-cdk/applet-js/test/test-applet.ts b/packages/@aws-cdk/applet-js/test/test-applet.ts index 685fbc98f1422..6d228770db998 100644 --- a/packages/@aws-cdk/applet-js/test/test-applet.ts +++ b/packages/@aws-cdk/applet-js/test/test-applet.ts @@ -1,4 +1,4 @@ -import { App, Construct, Parameter, Stack, StackProps } from '@aws-cdk/cdk'; +import { App, Parameter, Stack, StackProps } from '@aws-cdk/cdk'; export interface TestAppletProps extends StackProps { prop1: string @@ -30,15 +30,3 @@ export class Applet extends Stack { this.templateOptions.description = props.desc; } } - -interface NoStackAppletProps { - argument: string; -} - -export class NoStackApplet extends Construct { - constructor(parent: Construct, id: string, props: NoStackAppletProps) { - super(parent, id); - - new Parameter(this, 'P1', { default: props.argument, type: 'String' }); - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/applet-js/test/test-fromnpm.yaml b/packages/@aws-cdk/applet-js/test/test-fromnpm.yaml deleted file mode 100644 index 5ddc286f66755..0000000000000 --- a/packages/@aws-cdk/applet-js/test/test-fromnpm.yaml +++ /dev/null @@ -1 +0,0 @@ -applet: npm:// diff --git a/packages/@aws-cdk/applet-js/test/test-multistack-expected.json b/packages/@aws-cdk/applet-js/test/test-multistack-expected.json deleted file mode 100644 index d3cfb592f5396..0000000000000 --- a/packages/@aws-cdk/applet-js/test/test-multistack-expected.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Stack2", - "template": { - "Parameters": { - "P1": { - "Type": "String", - "Default": "stack2" - }, - "P2": { - "Type": "Number", - "Default": 456 - } - } - }, - "metadata": { - "/Stack2/P1": [ - { - "type": "aws:cdk:logicalId", - "data": "P1", - "trace": [ - "**REDACTED**" - ] - } - ], - "/Stack2/P2": [ - { - "type": "aws:cdk:logicalId", - "data": "P2", - "trace": [ - "**REDACTED**" - ] - } - ] - } -} diff --git a/packages/@aws-cdk/applet-js/test/test-multistack.yaml b/packages/@aws-cdk/applet-js/test/test-multistack.yaml deleted file mode 100644 index e2aec470d1a3a..0000000000000 --- a/packages/@aws-cdk/applet-js/test/test-multistack.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- applet: ./test-applet:TestApplet - name: Stack1 - prop1: stack1 - prop2: 123 -- applet: ./test-applet:TestApplet - name: Stack2 - prop1: stack2 - prop2: 456 diff --git a/packages/@aws-cdk/applet-js/test/test-nonstack-expected.json b/packages/@aws-cdk/applet-js/test/test-nonstack-expected.json deleted file mode 100644 index 3abdc38dc8187..0000000000000 --- a/packages/@aws-cdk/applet-js/test/test-nonstack-expected.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "NoStackApplet", - "template": { - "Parameters": { - "P1": { - "Type": "String", - "Default": "this should be reflected in the template description" - } - } - }, - "metadata": { - "/NoStackApplet/Default/P1": [ - { - "type": "aws:cdk:logicalId", - "data": "P1", - "trace": [ - "**REDACTED**" - ] - } - ] - } -} diff --git a/packages/@aws-cdk/applet-js/test/test-nonstack.yaml b/packages/@aws-cdk/applet-js/test/test-nonstack.yaml deleted file mode 100644 index c9dbcdd7f6170..0000000000000 --- a/packages/@aws-cdk/applet-js/test/test-nonstack.yaml +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env cdk-applet-js -applet: ./test-applet:NoStackApplet -argument: this should be reflected in the template description - diff --git a/packages/@aws-cdk/applet-js/test/test.applets.ts b/packages/@aws-cdk/applet-js/test/test.applets.ts deleted file mode 100644 index a461eacfd8509..0000000000000 --- a/packages/@aws-cdk/applet-js/test/test.applets.ts +++ /dev/null @@ -1,172 +0,0 @@ -import child_process = require('child_process'); -import fs = require('fs'); -import { Test } from 'nodeunit'; -import os = require('os'); -import path = require('path'); - -export = { - 'basic test 1'(test: Test) { - expectMatch(test, 'test/expected1.json', getStack('TestApplet', synthesizeApplet('test/test1.yaml'))); - test.done(); - }, - - 'basic test 2'(test: Test) { - expectMatch(test, 'test/expected2.json', getStack('TestApplet', synthesizeApplet('test/test2.yaml'))); - test.done(); - }, - - 'can use shebang'(test: Test) { - fs.chmodSync('test/test3.yaml', 0o755); - expectMatch(test, 'test/expected3.json', getStack('Applet', synthesizeApplet('test/test3.yaml', true))); - test.done(); - }, - - 'test non stack construct'(test: Test) { - expectMatch(test, 'test/test-nonstack-expected.json', getStack('NoStackApplet', synthesizeApplet('test/test-nonstack.yaml'))); - test.done(); - }, - - 'test multiple stacks'(test: Test) { - expectMatch(test, 'test/test-multistack-expected.json', getStack('Stack2', synthesizeApplet('test/test-multistack.yaml'))); - test.done(); - }, - - 'expect failure 4'(test: Test) { - test.throws(() => { - synthesizeApplet('test/negative-test4.yaml'); - }, /but it must be in the form/); - test.done(); - }, - - 'expect failure 5'(test: Test) { - test.throws(() => { - synthesizeApplet('test/negative-test5.yaml'); - }, /Cannot find module/); - test.done(); - }, - - 'expect failure 6'(test: Test) { - test.throws(() => { - synthesizeApplet('test/negative-test6.yaml'); - }, /Cannot find applet class/); - test.done(); - }, - - 'expect failure 7'(test: Test) { - test.throws(() => { - synthesizeApplet('test/negative-test7.yaml'); - }, /but it must be in the form/); - test.done(); - }, -}; - -function expectMatch(test: Test, expectedFile: string, stack: any) { - try { - const expected = JSON.parse(fs.readFileSync(expectedFile, { encoding: 'utf-8' })); - test.deepEqual(stack, expected); - } catch (e) { - if (e.code === 'ENOENT') { - // tslint:disable-next-line:no-console - console.log(JSON.stringify(stack, undefined, 2)); - throw new Error(`Make a file ${expectedFile} with the previous contents`); - } - } -} - -function synthesizeApplet(yamlFile: string, direct = false) { - // Can't depend on aws-cdk here, so we're reimplementing cx-api. - // tslint:disable-next-line:no-console - console.log('Writing to ', os.tmpdir()); - - const command = direct ? yamlFile : 'cdk-applet-js'; - const args = direct ? [] : [yamlFile]; - - child_process.execFileSync(command, args, { - env: { - ...process.env, - CDK_OUTDIR: os.tmpdir(), - PATH: 'bin:' + process.env.PATH - } - }); - - return JSON.parse(fs.readFileSync(path.join(os.tmpdir(), 'cdk.out'), { encoding: 'utf-8' })); -} - -function getStack(stackName: string, allStacks: any) { - for (const stack of allStacks.stacks) { - if (stack.name === stackName) { - return stripStackMetadata(stack); - } - } - - // tslint:disable-next-line:no-console - console.log(allStacks); - throw new Error('Could not find stack: ' + stackName); -} - -function stripStackMetadata(stack: any) { - for (const key of Object.keys(stack.metadata)) { - if (!stack.metadata[key]) { continue; } - for (const entry of (stack.metadata[key] as any[])) { - if (entry.trace) { entry.trace = ['**REDACTED**']; } - } - } - delete stack.environment; - return stack; -} - -// cdk-applet-js test1.yaml '{ "type": "synth", "stacks": ["TestApplet"] }' | node strip-stacktrace.js > /tmp/actual1.json -// expect_success diff expected1.json /tmp/actual1.json - -// #!/bin/bash -// set -euo pipefail -// cd $(dirname $0) -// export PATH=../bin:$PATH - -// announce() { -// echo "-------------------------------------------------" -// echo "$@" -// } - -// expect_success() { -// announce $@ -// set +e -// $@ -// local exit_code=$? -// set -e - -// if [ "${exit_code}" -ne 0 ]; then -// echo "Command expected to succeed: $@" -// exit 1 -// fi -// } - -// expect_failure() { -// announce $@ -// set +e -// $@ -// local exit_code=$? -// set -e -// if [ "${exit_code}" -eq 0 ]; then -// echo "Command expected to fail: $@" -// exit 1 -// fi -// } - -// cdk-applet-js test1.yaml '{ "type": "synth", "stacks": ["TestApplet"] }' | node strip-stacktrace.js > /tmp/actual1.json -// expect_success diff expected1.json /tmp/actual1.json - -// cdk-applet-js test2.yaml '{ "type": "synth", "stacks": ["TestApplet"] }' | node strip-stacktrace.js > /tmp/actual2.json -// expect_success diff expected2.json /tmp/actual2.json - -// # applets can use the host as shebang -// chmod +x ./test3.yaml # since codebuild loses permissions -// ./test3.yaml '{ "type": "synth", "stacks": ["Applet"] }' | node strip-stacktrace.js > /tmp/actual3.json -// expect_success diff expected3.json /tmp/actual3.json - -// expect_failure cdk-applet-js negative-test4.yaml -// expect_failure cdk-applet-js negative-test5.yaml -// expect_failure cdk-applet-js negative-test6.yaml -// expect_failure cdk-applet-js negative-test7.yaml - -// echo "PASS" \ No newline at end of file diff --git a/packages/@aws-cdk/applet-js/test/test.helpers.ts b/packages/@aws-cdk/applet-js/test/test.helpers.ts deleted file mode 100644 index 31d03ce711c46..0000000000000 --- a/packages/@aws-cdk/applet-js/test/test.helpers.ts +++ /dev/null @@ -1,56 +0,0 @@ -import cdk = require('@aws-cdk/cdk'); -import { Test } from 'nodeunit'; -import { extractModuleName, isStackConstructor, parseApplet } from '../lib/applet-helpers'; - -export = { - 'test that refactoring Stack didn\'t break Stack detection'(test: Test) { - test.equals(true, isStackConstructor(cdk.Stack)); - test.done(); - }, - - 'test package name extraction'(test: Test) { - test.equals('my-package', extractModuleName('my-package')); - test.equals('my-package', extractModuleName('my-package@1.0')); - test.equals('@scope/my-package', extractModuleName('@scope/my-package')); - test.equals('@scope/my-package', extractModuleName('@scope/my-package@1.0')); - test.done(); - }, - - 'test applet name extraction'(test: Test) { - test.deepEqual(parseApplet('applet'), { - moduleName: 'applet', - className: 'Applet' - }); - - test.deepEqual(parseApplet('applet:Class'), { - moduleName: 'applet', - className: 'Class' - }); - - test.deepEqual(parseApplet('npm://applet:Class'), { - npmPackage: 'applet', - moduleName: 'applet', - className: 'Class' - }); - - test.deepEqual(parseApplet('npm://applet@1.0:Class'), { - npmPackage: 'applet@1.0', - moduleName: 'applet', - className: 'Class' - }); - - test.deepEqual(parseApplet('npm://applet@1.0'), { - npmPackage: 'applet@1.0', - moduleName: 'applet', - className: 'Applet' - }); - - test.deepEqual(parseApplet('npm://@scope/applet@1.0'), { - npmPackage: '@scope/applet@1.0', - moduleName: '@scope/applet', - className: 'Applet' - }); - - test.done(); - } -}; \ No newline at end of file diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index d5a18f1587adb..256a5d3bc6b61 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -2,10 +2,14 @@ import 'source-map-support/register'; import cxapi = require('@aws-cdk/cx-api'); +import childProcess = require('child_process'); import colors = require('colors/safe'); import fs = require('fs-extra'); import YAML = require('js-yaml'); import minimatch = require('minimatch'); +import os = require('os'); +import path = require('path'); +import semver = require('semver'); import util = require('util'); import yargs = require('yargs'); import cdkUtil = require('../lib/util'); @@ -13,13 +17,12 @@ import cdkUtil = require('../lib/util'); import { bootstrapEnvironment, deployStack, destroyStack, loadToolkitInfo, Mode, SDK } from '../lib'; import contextplugins = require('../lib/contextplugins'); import { printStackDiff } from '../lib/diff'; -import { execProgram } from '../lib/exec'; import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init'; import { interactive } from '../lib/interactive'; import { data, debug, error, highlight, print, setVerbose, success, warning } from '../lib/logging'; import { PluginHost } from '../lib/plugin'; import { parseRenames } from '../lib/renames'; -import { DEFAULTS, PER_USER_DEFAULTS, Settings } from '../lib/settings'; +import { Settings } from '../lib/settings'; import { VERSION } from '../lib/version'; // tslint:disable-next-line:no-var-requires @@ -27,6 +30,9 @@ const promptly = require('promptly'); const DEFAULT_TOOLKIT_STACK_NAME = 'CDKToolkit'; +const DEFAULTS = 'cdk.json'; +const PER_USER_DEFAULTS = '~/.cdk.json'; + /** * Since app execution basically always synthesizes all the stacks, * we can invoke it once and cache the response for subsequent calls. @@ -378,7 +384,7 @@ async function initCommandLine() { // We may need to run the cloud executable multiple times in order to satisfy all missing context while (true) { - const response: cxapi.SynthesizeResponse = await execProgram(aws, config); + const response: cxapi.SynthesizeResponse = await execProgram(); const allMissing = cdkUtil.deepMerge(...response.stacks.map(s => s.missing)); if (!cdkUtil.isEmpty(allMissing)) { @@ -435,6 +441,105 @@ async function initCommandLine() { } } + /** Invokes the cloud executable and returns JSON output */ + async function execProgram(): Promise { + const env: { [key: string]: string } = { }; + + const context = config.get(['context']); + await populateDefaultEnvironmentIfNeeded(context); + + env[cxapi.CONTEXT_ENV] = JSON.stringify(context); + + const app = config.get(['app']); + if (!app) { + throw new Error(`--app is required either in command-line, in ${DEFAULTS} or in ${PER_USER_DEFAULTS}`); + } + + const commandLine = appToArray(app); + + const outdir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk')); + debug('outdir:', outdir); + env[cxapi.OUTDIR_ENV] = outdir; + + try { + const outfile = await exec(); + debug('outfile:', outfile); + if (!(await fs.pathExists(outfile))) { + throw new Error(`Unable to find output file ${outfile}`); + } + + const response = await fs.readJson(outfile); + debug(response); + return versionCheckResponse(response); + } finally { + debug('Removing outdir', outdir); + await fs.remove(outdir); + } + + async function exec() { + return new Promise((ok, fail) => { + // We use a slightly lower-level interface to: + // + // - Pass arguments in an array instead of a string, to get around a + // number of quoting issues introduced by the intermediate shell layer + // (which would be different between Linux and Windows). + // + // - Inherit stderr from controlling terminal. We don't use the captured value + // anway, and if the subprocess is printing to it for debugging purposes the + // user gets to see it sooner. Plus, capturing doesn't interact nicely with some + // processes like Maven. + const proc = childProcess.spawn(commandLine[0], commandLine.slice(1), { + stdio: ['ignore', 'inherit', 'inherit'], + detached: false, + env: { + ...process.env, + ...env + } + }); + + proc.on('error', fail); + + proc.on('exit', code => { + if (code === 0) { + return ok(path.join(outdir, cxapi.OUTFILE_NAME)); + } else { + return fail(new Error('Subprocess exited with error ' + code.toString())); + } + }); + }); + } + } + } + + /** + * Look at the type of response we get and upgrade it to the latest expected version + */ + function versionCheckResponse(response: cxapi.SynthesizeResponse): cxapi.SynthesizeResponse { + if (!response.version) { + // tslint:disable-next-line:max-line-length + throw new Error(`CDK Framework >= ${cxapi.PROTO_RESPONSE_VERSION} is required in order to interact with this version of the Toolkit.`); + } + + const frameworkVersion = semver.coerce(response.version); + const toolkitVersion = semver.coerce(cxapi.PROTO_RESPONSE_VERSION); + + // Should not happen, but I don't trust this library 100% either, so let's check for it to be safe + if (!frameworkVersion || !toolkitVersion) { throw new Error('SemVer library could not parse versions'); } + + if (semver.gt(frameworkVersion, toolkitVersion)) { + throw new Error(`CDK Toolkit >= ${response.version} is required in order to interact with this program.`); + } + + if (semver.lt(frameworkVersion, toolkitVersion)) { + // Toolkit protocol is newer than the framework version, and we KNOW the + // version. This is a scenario in which we could potentially do some + // upgrading of the response in the future. + // + // For now though, we simply reject old responses. + throw new Error(`CDK Framework >= ${cxapi.PROTO_RESPONSE_VERSION} is required in order to interact with this version of the Toolkit.`); + } + + return response; } /** @@ -662,6 +767,39 @@ async function initCommandLine() { }); } + /** + * Make sure the 'app' is an array + * + * If it's a string, split on spaces as a trivial way of tokenizing the command line. + */ + function appToArray(app: any) { + return typeof app === 'string' ? app.split(' ') : app; + } + + /** + * If we don't have region/account defined in context, we fall back to the default SDK behavior + * where region is retreived from ~/.aws/config and account is based on default credentials provider + * chain and then STS is queried. + * + * This is done opportunistically: for example, if we can't acccess STS for some reason or the region + * is not configured, the context value will be 'null' and there could failures down the line. In + * some cases, synthesis does not require region/account information at all, so that might be perfectly + * fine in certain scenarios. + * + * @param context The context key/value bash. + */ + async function populateDefaultEnvironmentIfNeeded(context: any) { + if (!(cxapi.DEFAULT_REGION_CONTEXT_KEY in context)) { + context[cxapi.DEFAULT_REGION_CONTEXT_KEY] = await aws.defaultRegion(); + debug(`Setting "${cxapi.DEFAULT_REGION_CONTEXT_KEY}" context to`, context[cxapi.DEFAULT_REGION_CONTEXT_KEY]); + } + + if (!(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY in context)) { + context[cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY] = await aws.defaultAccount(); + debug(`Setting "${cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY}" context to`, context[cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY]); + } + } + /** * Combine the names of a set of stacks using a comma */ diff --git a/packages/aws-cdk/lib/exec.ts b/packages/aws-cdk/lib/exec.ts deleted file mode 100644 index 50bc2007bf438..0000000000000 --- a/packages/aws-cdk/lib/exec.ts +++ /dev/null @@ -1,154 +0,0 @@ -import cxapi = require('@aws-cdk/cx-api'); -import childProcess = require('child_process'); -import fs = require('fs-extra'); -import os = require('os'); -import path = require('path'); -import semver = require('semver'); -import { DEFAULTS, PER_USER_DEFAULTS, Settings } from '../lib/settings'; -import { SDK } from './api'; -import { debug } from './logging'; - -/** Invokes the cloud executable and returns JSON output */ -export async function execProgram(aws: SDK, config: Settings): Promise { - const env: { [key: string]: string } = { }; - - const context = config.get(['context']); - await populateDefaultEnvironmentIfNeeded(aws, context); - - env[cxapi.CONTEXT_ENV] = JSON.stringify(context); - - const app = config.get(['app']); - if (!app) { - throw new Error(`--app is required either in command-line, in ${DEFAULTS} or in ${PER_USER_DEFAULTS}`); - } - - const commandLine = await guessExecutable(appToArray(app)); - - const outdir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk')); - debug('outdir:', outdir); - env[cxapi.OUTDIR_ENV] = outdir; - - try { - const outfile = await exec(); - debug('outfile:', outfile); - if (!(await fs.pathExists(outfile))) { - throw new Error(`Unable to find output file ${outfile}`); - } - - const response = await fs.readJson(outfile); - debug(response); - return versionCheckResponse(response); - } finally { - debug('Removing outdir', outdir); - await fs.remove(outdir); - } - - async function exec() { - return new Promise((ok, fail) => { - // We use a slightly lower-level interface to: - // - // - Pass arguments in an array instead of a string, to get around a - // number of quoting issues introduced by the intermediate shell layer - // (which would be different between Linux and Windows). - // - // - Inherit stderr from controlling terminal. We don't use the captured value - // anway, and if the subprocess is printing to it for debugging purposes the - // user gets to see it sooner. Plus, capturing doesn't interact nicely with some - // processes like Maven. - const proc = childProcess.spawn(commandLine[0], commandLine.slice(1), { - stdio: ['ignore', 'inherit', 'inherit'], - detached: false, - env: { - ...process.env, - ...env - } - }); - - proc.on('error', fail); - - proc.on('exit', code => { - if (code === 0) { - return ok(path.join(outdir, cxapi.OUTFILE_NAME)); - } else { - return fail(new Error('Subprocess exited with error ' + code.toString())); - } - }); - }); - } -} - -/** - * Look at the type of response we get and upgrade it to the latest expected version - */ -function versionCheckResponse(response: cxapi.SynthesizeResponse): cxapi.SynthesizeResponse { - if (!response.version) { - // tslint:disable-next-line:max-line-length - throw new Error(`CDK Framework >= ${cxapi.PROTO_RESPONSE_VERSION} is required in order to interact with this version of the Toolkit.`); - } - - const frameworkVersion = semver.coerce(response.version); - const toolkitVersion = semver.coerce(cxapi.PROTO_RESPONSE_VERSION); - - // Should not happen, but I don't trust this library 100% either, so let's check for it to be safe - if (!frameworkVersion || !toolkitVersion) { throw new Error('SemVer library could not parse versions'); } - - if (semver.gt(frameworkVersion, toolkitVersion)) { - throw new Error(`CDK Toolkit >= ${response.version} is required in order to interact with this program.`); - } - - if (semver.lt(frameworkVersion, toolkitVersion)) { - // Toolkit protocol is newer than the framework version, and we KNOW the - // version. This is a scenario in which we could potentially do some - // upgrading of the response in the future. - // - // For now though, we simply reject old responses. - throw new Error(`CDK Framework >= ${cxapi.PROTO_RESPONSE_VERSION} is required in order to interact with this version of the Toolkit.`); - } - - return response; -} - -/** - * If we don't have region/account defined in context, we fall back to the default SDK behavior - * where region is retreived from ~/.aws/config and account is based on default credentials provider - * chain and then STS is queried. - * - * This is done opportunistically: for example, if we can't acccess STS for some reason or the region - * is not configured, the context value will be 'null' and there could failures down the line. In - * some cases, synthesis does not require region/account information at all, so that might be perfectly - * fine in certain scenarios. - * - * @param context The context key/value bash. - */ -async function populateDefaultEnvironmentIfNeeded(aws: SDK, context: any) { - if (!(cxapi.DEFAULT_REGION_CONTEXT_KEY in context)) { - context[cxapi.DEFAULT_REGION_CONTEXT_KEY] = await aws.defaultRegion(); - debug(`Setting "${cxapi.DEFAULT_REGION_CONTEXT_KEY}" context to`, context[cxapi.DEFAULT_REGION_CONTEXT_KEY]); - } - - if (!(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY in context)) { - context[cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY] = await aws.defaultAccount(); - debug(`Setting "${cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY}" context to`, context[cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY]); - } -} - -/** - * Make sure the 'app' is an array - * - * If it's a string, split on spaces as a trivial way of tokenizing the command line. - */ -function appToArray(app: any) { - return typeof app === 'string' ? app.split(' ') : app; -} - -/** - * Guess the executable from the command-line argument - */ -async function guessExecutable(commandLine: string[]) { - if (commandLine.length === 1 && ['.yml', '.yaml'].includes(path.extname(commandLine[0]))) { - // Direct execution of a YAML file, assume that we're deploying an Applet - const appletBinary = path.resolve(require.resolve('@aws-cdk/applet-js')); - return [process.execPath, appletBinary, commandLine[0]]; - } - return commandLine; -} \ No newline at end of file diff --git a/packages/aws-cdk/lib/settings.ts b/packages/aws-cdk/lib/settings.ts index a6200cbfee399..4c6a63ae39309 100644 --- a/packages/aws-cdk/lib/settings.ts +++ b/packages/aws-cdk/lib/settings.ts @@ -6,9 +6,6 @@ import util = require('./util'); export type SettingsMap = {[key: string]: any}; -export const DEFAULTS = 'cdk.json'; -export const PER_USER_DEFAULTS = '~/.cdk.json'; - export class Settings { public static mergeAll(...settings: Settings[]): Settings { let ret = new Settings(); diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 073e965c4c4bc..29c3540d016e8 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -47,7 +47,6 @@ "dependencies": { "@aws-cdk/cloudformation-diff": "^0.14.1", "@aws-cdk/cx-api": "^0.14.1", - "@aws-cdk/applet-js": "^0.14.1", "archiver": "^2.1.1", "aws-sdk": "^2.259.1", "camelcase": "^5.0.0", From 4f561cdb755fc8ac0a8eaad11049dfd992bfcd1d Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 31 Oct 2018 23:40:11 +0100 Subject: [PATCH 104/140] Run build --- packages/@aws-cdk/aws-ecs/lib/container-definition.ts | 8 ++++---- packages/@aws-cdk/aws-ecs/package.json | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 3a1cc79ae9916..bd933de9de04f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -208,14 +208,14 @@ export class ContainerDefinition extends cdk.Construct { public readonly essential: boolean; /** - * The configured container links + * Whether there was at least one memory limit specified in this definition */ - private readonly links = new Array(); + public readonly memoryLimitSpecified: boolean; /** - * Whether there was at least one memory limit specified in this definition + * The configured container links */ - public readonly memoryLimitSpecified: boolean; + private readonly links = new Array(); /** * The task definition this container definition is part of diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 31e66a9395644..f20305510f1b3 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -67,6 +67,7 @@ "@aws-cdk/aws-elasticloadbalancing": "^0.14.1", "@aws-cdk/aws-elasticloadbalancingv2": "^0.14.1", "@aws-cdk/aws-iam": "^0.14.1", + "@aws-cdk/aws-lambda": "^0.14.1", "@aws-cdk/aws-logs": "^0.14.1", "@aws-cdk/cdk": "^0.14.1", "@aws-cdk/cx-api": "^0.14.1" From 47e52510084a88bc1fff27dd8fd94e1c93b8e29a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 31 Oct 2018 23:48:07 +0100 Subject: [PATCH 105/140] Fix tests --- packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts | 3 ++- packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts | 3 ++- packages/@aws-cdk/aws-ecs/test/test.container-definition.ts | 1 + packages/@aws-cdk/aws-ecs/test/test.l3s.ts | 3 ++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts index 3af4c35645bf6..80545e50ed452 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts @@ -399,6 +399,7 @@ export = { const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TD', { networkMode: ecs.NetworkMode.Host }); const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image('test'), + memoryLimitMiB: 1024, }); container.addPortMappings({ containerPort: 808 }); const service = new ecs.EcsService(stack, 'Service', { cluster, taskDefinition }); @@ -421,4 +422,4 @@ export = { test.done(); }, } -}; \ No newline at end of file +}; diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts index d5a3b2b7c0da3..ff2a0f4ed93d9 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts @@ -149,6 +149,7 @@ export = { }); taskDefinition.addContainer("web", { + memoryLimitMiB: 1024, image: ecs.DockerHub.image("amazon/amazon-ecs-sample") }); @@ -206,4 +207,4 @@ export = { // test.done(); // }, } -}; \ No newline at end of file +}; diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index 19819a1ca3cce..dba82d0b35982 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -216,6 +216,7 @@ export = { // WHEN taskDefinition.addContainer('cont', { image: ecs.DockerHub.image('test'), + memoryLimitMiB: 1024, logging: new ecs.AwsLogDriver(stack, 'Logging', { streamPrefix: 'prefix' }) }); diff --git a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts index 2ce7bcf1654c3..125cc5736f771 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts @@ -14,6 +14,7 @@ export = { // WHEN new ecs.LoadBalancedEcsService(stack, 'Service', { cluster, + memoryLimitMiB: 1024, image: ecs.DockerHub.image('test') }); @@ -40,4 +41,4 @@ export = { test.done(); } -}; \ No newline at end of file +}; From 6152ddcffd9b66219994ea24d44c9215f2130032 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 31 Oct 2018 15:53:45 -0700 Subject: [PATCH 106/140] Update readme --- packages/@aws-cdk/aws-ecs/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 9943a85cedd00..1c8f441515385 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -31,9 +31,9 @@ one to run Tasks on fargate. ## Cluster -An `EcsCluster` or `FargateCluster` defines a set of instances to run your +An `EcsCluster` or `FargateCluster` defines the infrastructure to run your tasks on. If you create an ECS cluster, an AutoScalingGroup of EC2 instances -running the right AMI will implicitly be created for you. +running the latest ECS Optimized AMI will implicitly be created for you. You can run many tasks on a single cluster. @@ -73,7 +73,7 @@ taskDefinition.addContainer('main', { Images supply the software that runs inside the container. Images can be obtained from either DockerHub or from ECR repositories: -* `ecs.DockerHub.image(imageName)`: use an publicly available image from +* `ecs.DockerHub.image(imageName)`: use a publicly available image from DockerHub. * `repository.getImage(tag)`: use the given ECR repository as the image to start. @@ -81,7 +81,7 @@ obtained from either DockerHub or from ECR repositories: ## Service A `Service` instantiates a `TaskDefinition` on a `Cluster` a given number of -times, optionally associating them with a load balnacer. Tasks that fail will +times, optionally associating them with a load balancer. Tasks that fail will automatically be restarted. ```ts From 62ff3a4048084e125d851ccda48da8310fd23034 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 31 Oct 2018 16:39:27 -0700 Subject: [PATCH 107/140] Remove cdk.json files --- .../hello-cdk-ecs/cdk.json | 42 ----------------- .../hello-cdk-fargate/cdk.json | 47 ------------------- 2 files changed, 89 deletions(-) delete mode 100644 examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json delete mode 100644 examples/cdk-examples-typescript/hello-cdk-fargate/cdk.json diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json b/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json deleted file mode 100644 index eb5f700d36513..0000000000000 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "app": "node index", - "context": { - "availability-zones:585695036304:us-east-1": [ - "us-east-1a", - "us-east-1b", - "us-east-1c", - "us-east-1d", - "us-east-1e", - "us-east-1f" - ], - "ssm:585695036304:us-east-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-14c5486b", - "availability-zones:585695036304:eu-west-2": [ - "eu-west-2a", - "eu-west-2b", - "eu-west-2c" - ], - "ssm:585695036304:eu-west-2:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-a36f8dc4", - "availability-zones:585695036304:eu-west-1": [ - "eu-west-1a", - "eu-west-1b", - "eu-west-1c" - ], - "ssm:585695036304:eu-west-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-ca0135b3", - "availability-zones:794715269151:us-west-2": [ - "us-west-2a", - "us-west-2b", - "us-west-2c" - ], - "availability-zones:993655754359:us-west-2": [ - "us-west-2a", - "us-west-2b", - "us-west-2c" - ], - "availability-zones:993655754359:eu-west-1": [ - "eu-west-1a", - "eu-west-1b", - "eu-west-1c" - ], - "ssm:794715269151:us-west-2:/aws/service/ecs/optimized-ami/amazon-linux/recommended": "{\"schema_version\":1,\"image_name\":\"amzn-ami-2018.03.g-amazon-ecs-optimized\",\"image_id\":\"ami-00430184c7bb49914\",\"os\":\"Amazon Linux\",\"ecs_runtime_version\":\"Docker version 18.06.1-ce\",\"ecs_agent_version\":\"1.20.3\"}" - } -} diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/cdk.json b/examples/cdk-examples-typescript/hello-cdk-fargate/cdk.json deleted file mode 100644 index 65f81e1c0d3ee..0000000000000 --- a/examples/cdk-examples-typescript/hello-cdk-fargate/cdk.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "app": "node index", - "context": { - "availability-zones:585695036304:us-east-1": [ - "us-east-1a", - "us-east-1b", - "us-east-1c", - "us-east-1d", - "us-east-1e", - "us-east-1f" - ], - "ssm:585695036304:us-east-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-14c5486b", - "availability-zones:585695036304:eu-west-2": [ - "eu-west-2a", - "eu-west-2b", - "eu-west-2c" - ], - "ssm:585695036304:eu-west-2:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-a36f8dc4", - "availability-zones:585695036304:eu-west-1": [ - "eu-west-1a", - "eu-west-1b", - "eu-west-1c" - ], - "ssm:585695036304:eu-west-1:/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2": "ami-ca0135b3", - "availability-zones:794715269151:us-west-2": [ - "us-west-2a", - "us-west-2b", - "us-west-2c" - ], - "availability-zones:993655754359:us-west-2": [ - "us-west-2a", - "us-west-2b", - "us-west-2c" - ], - "availability-zones:993655754359:eu-west-1": [ - "eu-west-1a", - "eu-west-1b", - "eu-west-1c" - ], - "ssm:794715269151:us-west-2:/aws/service/ecs/optimized-ami/amazon-linux/recommended": "{\"schema_version\":1,\"image_name\":\"amzn-ami-2018.03.g-amazon-ecs-optimized\",\"image_id\":\"ami-00430184c7bb49914\",\"os\":\"Amazon Linux\",\"ecs_runtime_version\":\"Docker version 18.06.1-ce\",\"ecs_agent_version\":\"1.20.3\"}", - "availability-zones:794715269151:eu-west-1": [ - "eu-west-1a", - "eu-west-1b", - "eu-west-1c" - ] - } -} From b2fa1dbb4941e146839da58e188979d8add4b80e Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 31 Oct 2018 19:17:11 -0700 Subject: [PATCH 108/140] Update error message --- packages/@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 4ed25d03085fc..659046290af4d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -214,7 +214,7 @@ export abstract class BaseService extends cdk.Construct */ private attachToELBv2(targetGroup: elbv2.ITargetGroup): elbv2.LoadBalancerTargetProps { if (this.taskDef.networkMode === NetworkMode.None) { - throw new Error("Cannot use a load balancer if NetworkMode is None. Use Host or AwsVpc instead."); + throw new Error("Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead."); } this.loadBalancers.push({ From 410d263dc67d1e70f7071cafb7d2ba5741a0ef17 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 31 Oct 2018 20:13:45 -0700 Subject: [PATCH 109/140] Rename Ecs resources to Ec2* This is to ease confusion around Fargate also being implemented on top of ECS and to be more explicit that the customer will be owning EC2 capacity in their ECS cluster. --- .../hello-cdk-ecs/index.ts | 82 +---------------- packages/@aws-cdk/aws-ecs/README.md | 12 +-- .../ecs-cluster.ts => ec2/ec2-cluster.ts} | 24 ++--- .../ecs-service.ts => ec2/ec2-service.ts} | 18 ++-- .../ec2-task-definition.ts} | 6 +- packages/@aws-cdk/aws-ecs/lib/index.ts | 6 +- .../aws-ecs/lib/load-balanced-ecs-service.ts | 20 ++--- .../test/{ecs => ec2}/integ.asset-image.ts | 6 +- .../test/{ecs => ec2}/integ.lb-awsvpc-nw.ts | 6 +- .../test/{ecs => ec2}/integ.lb-bridge-nw.ts | 6 +- .../test.ec2-cluster.ts} | 20 ++--- .../test.ec2-service.ts} | 90 +++++++++---------- .../test.ec2-task-definition.ts} | 20 ++--- .../aws-ecs/test/test.container-definition.ts | 18 ++-- packages/@aws-cdk/aws-ecs/test/test.l3s.ts | 4 +- 15 files changed, 131 insertions(+), 207 deletions(-) rename packages/@aws-cdk/aws-ecs/lib/{ecs/ecs-cluster.ts => ec2/ec2-cluster.ts} (91%) rename packages/@aws-cdk/aws-ecs/lib/{ecs/ecs-service.ts => ec2/ec2-service.ts} (94%) rename packages/@aws-cdk/aws-ecs/lib/{ecs/ecs-task-definition.ts => ec2/ec2-task-definition.ts} (93%) rename packages/@aws-cdk/aws-ecs/test/{ecs => ec2}/integ.asset-image.ts (73%) rename packages/@aws-cdk/aws-ecs/test/{ecs => ec2}/integ.lb-awsvpc-nw.ts (84%) rename packages/@aws-cdk/aws-ecs/test/{ecs => ec2}/integ.lb-bridge-nw.ts (84%) rename packages/@aws-cdk/aws-ecs/test/{ecs/test.ecs-cluster.ts => ec2/test.ec2-cluster.ts} (89%) rename packages/@aws-cdk/aws-ecs/test/{ecs/test.ecs-service.ts => ec2/test.ec2-service.ts} (76%) rename packages/@aws-cdk/aws-ecs/test/{ecs/test.ecs-task-definition.ts => ec2/test.ec2-task-definition.ts} (89%) diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 7d823061cd580..0a103e5619dec 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -12,14 +12,14 @@ class BonjourECS extends cdk.Stack { // deploy, but VPC creation is slow so we'll only have to do that once // and can iterate quickly on consuming stacks. Not doing that for now. const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); - const cluster = new ecs.EcsCluster(this, 'EcsCluster', { + const cluster = new ecs.Ec2Cluster(this, 'Ec2Cluster', { vpc, size: 3, instanceType: new InstanceType("t2.xlarge") }); // Instantiate ECS Service with just cluster and image - const ecsService = new ecs.LoadBalancedEcsService(this, "EcsService", { + const ecsService = new ecs.LoadBalancedEc2Service(this, "Ec2Service", { cluster, memoryLimitMiB: 512, image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), @@ -34,80 +34,4 @@ const app = new cdk.App(); new BonjourECS(app, 'Bonjour'); -app.run(); - -// name, image, cpu, memory, port (with default) -// -// Include in constructs: -// - networking - include SD, ALB -// - logging - cloudwatch logs integration? talk to nathan about 3rd -// party integrations - aggregated logging across the service -// (instead of per task). Probably prometheus or elk? -// - tracing aws-xray-fargate - CNCF opentracing standard - jaeger, -// zipkin. -// - so x-ray is a container that is hooked up to sidecars that come -// with the application container itself -// - autoscaling - application autoscaling (Fargate focused?) - -// const taskDefinition = new ecs.EcsTaskDefinition(this, "EcsTD", { -// family: "ecs-task-definition", -// }); - -// const container = taskDefinition.addContainer('web', { -// image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), -// cpu: 1024, -// memoryLimitMiB: 512, -// essential: true -// }); - -// container.linuxParameters.addCapabilities(ecs.Capability.All); -// container.linuxParameters.dropCapabilities(ecs.Capability.Chown); - -// container.linuxParameters.addDevices({ -// containerPath: "/dev/pudding", -// hostPath: "/dev/clyde", -// permissions: [ecs.DevicePermission.Read] -// }); - -// container.linuxParameters.addTmpfs({ -// containerPath: "/dev/sda", -// size: 12345, -// mountOptions: [ecs.TmpfsMountOption.Ro] -// }); - -// container.linuxParameters.sharedMemorySize = 65535; -// container.linuxParameters.initProcessEnabled = true; - -// container.addUlimits({ -// name: ecs.UlimitName.Core, -// softLimit: 1234, -// hardLimit: 1234, -// }); - -// container.addPortMappings({ -// containerPort: 80, -// // hostPort: 80, -// protocol: ecs.Protocol.Tcp, -// }); - -// container.addMountPoints({ -// containerPath: '/tmp/cache', -// sourceVolume: 'volume-1', -// readOnly: true, -// }, { -// containerPath: './cache', -// sourceVolume: 'volume-2', -// readOnly: true, -// }); - -// container.addVolumesFrom({ -// sourceContainer: 'web', -// readOnly: true, -// }); - -// new ecs.EcsService(this, "EcsService", { -// cluster, -// taskDefinition, -// desiredCount: 1, -// }); -// } +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 1c8f441515385..893ebf1d15c7c 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -5,14 +5,14 @@ Service** (ECS). The simplest example of using this library looks like this: ```ts // Create an ECS cluster (backed by an AutoScaling group) -const cluster = new ecs.EcsCluster(this, 'Cluster', { +const cluster = new ecs.Ec2Cluster(this, 'Cluster', { vpc, size: 3, instanceType: new InstanceType("t2.xlarge") }); // Instantiate ECS Service with an automatic load balancer -const ecsService = new ecs.LoadBalancedEcsService(this, 'Service', { +const ecsService = new ecs.LoadBalancedEc2Service(this, 'Service', { cluster, memoryLimitMiB: 512, image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), @@ -24,14 +24,14 @@ const ecsService = new ecs.LoadBalancedEcsService(this, 'Service', { There are two sets of constructs in this library; one to run tasks on ECS and one to run Tasks on fargate. -- Use the `EcsCluster`, `EcsTaskDefinition` and `EcsService` constructs to +- Use the `Ec2Cluster`, `Ec2TaskDefinition` and `Ec2Service` constructs to run tasks on EC2 instances running in your account. - Use the `FargateCluster`, `FargateTaskDefinition` and `FargateService` constructs to run tasks on instances that are managed for you by AWS. ## Cluster -An `EcsCluster` or `FargateCluster` defines the infrastructure to run your +An `Ec2Cluster` or `FargateCluster` defines the infrastructure to run your tasks on. If you create an ECS cluster, an AutoScalingGroup of EC2 instances running the latest ECS Optimized AMI will implicitly be created for you. @@ -87,7 +87,7 @@ automatically be restarted. ```ts const taskDefinition; -const service = new ecs.EcsService(this, 'Service', { +const service = new ecs.Ec2Service(this, 'Service', { cluster, taskDefinition, desiredCount: 5 @@ -113,7 +113,7 @@ listener.addTargets('ECS', { There are two higher-level constructs available which include a load balancer for you: * `LoadBalancedFargateService` -* `LoadBalancedEcsService` +* `LoadBalancedEc2Service` ## Task AutoScaling diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts similarity index 91% rename from packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts rename to packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts index 64c9834c63e47..3b0ef66124508 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts @@ -8,7 +8,7 @@ import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; /** * Properties to define an ECS cluster */ -export interface EcsClusterProps extends BaseClusterProps { +export interface Ec2ClusterProps extends BaseClusterProps { /** * Whether or not the containers can access the instance role * @@ -32,12 +32,12 @@ export interface EcsClusterProps extends BaseClusterProps { /** * A container cluster that runs on your EC2 instances */ -export class EcsCluster extends BaseCluster implements IEcsCluster { +export class Ec2Cluster extends BaseCluster implements IEc2Cluster { /** * Import an existing cluster */ - public static import(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps): IEcsCluster { - return new ImportedEcsCluster(parent, name, props); + public static import(parent: cdk.Construct, name: string, props: ImportedEc2ClusterProps): IEc2Cluster { + return new ImportedEc2Cluster(parent, name, props); } /** @@ -50,7 +50,7 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { */ public readonly securityGroup: ec2.SecurityGroupRef; - constructor(parent: cdk.Construct, name: string, props: EcsClusterProps) { + constructor(parent: cdk.Construct, name: string, props: Ec2ClusterProps) { super(parent, name, props); const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'AutoScalingGroup', { @@ -97,9 +97,9 @@ export class EcsCluster extends BaseCluster implements IEcsCluster { } /** - * Export the EcsCluster + * Export the Ec2Cluster */ - public export(): ImportedEcsClusterProps { + public export(): ImportedEc2ClusterProps { return { clusterName: new cdk.Output(this, 'ClusterName', { value: this.clusterName }).makeImportValue().toString(), vpc: this.vpc.export(), @@ -162,7 +162,7 @@ export class EcsOptimizedAmi implements ec2.IMachineImageSource { /** * An ECS cluster */ -export interface IEcsCluster { +export interface IEc2Cluster { /** * Name of the cluster */ @@ -182,7 +182,7 @@ export interface IEcsCluster { /** * Properties to import an ECS cluster */ -export interface ImportedEcsClusterProps { +export interface ImportedEc2ClusterProps { /** * Name of the cluster */ @@ -200,9 +200,9 @@ export interface ImportedEcsClusterProps { } /** - * An EcsCluster that has been imported + * An Ec2Cluster that has been imported */ -class ImportedEcsCluster extends cdk.Construct implements IEcsCluster { +class ImportedEc2Cluster extends cdk.Construct implements IEc2Cluster { /** * Name of the cluster */ @@ -218,7 +218,7 @@ class ImportedEcsCluster extends cdk.Construct implements IEcsCluster { */ public readonly securityGroup: ec2.SecurityGroupRef; - constructor(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps) { + constructor(parent: cdk.Construct, name: string, props: ImportedEc2ClusterProps) { super(parent, name); this.clusterName = props.clusterName; this.vpc = ec2.VpcNetworkRef.import(this, "vpc", props.vpc); diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts similarity index 94% rename from packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts rename to packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index e472884657d66..7a685378f4c61 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -5,22 +5,22 @@ import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; -import { IEcsCluster } from './ecs-cluster'; -import { EcsTaskDefinition } from './ecs-task-definition'; +import { IEc2Cluster } from './ec2-cluster'; +import { Ec2TaskDefinition } from './ec2-task-definition'; /** * Properties to define an ECS service */ -export interface EcsServiceProps extends BaseServiceProps { +export interface Ec2ServiceProps extends BaseServiceProps { /** * Cluster where service will be deployed */ - cluster: IEcsCluster; + cluster: IEc2Cluster; /** * Task Definition used for running tasks in the service */ - taskDefinition: EcsTaskDefinition; + taskDefinition: Ec2TaskDefinition; /** * In what subnets to place the task's ENIs @@ -61,7 +61,7 @@ export interface EcsServiceProps extends BaseServiceProps { /** * Start a service on an ECS cluster */ -export class EcsService extends BaseService implements elb.ILoadBalancerTarget { +export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { /** * Manage allowed network traffic for this construct */ @@ -74,12 +74,12 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { protected readonly taskDef: BaseTaskDefinition; - private readonly taskDefinition: EcsTaskDefinition; + private readonly taskDefinition: Ec2TaskDefinition; private readonly constraints: cloudformation.ServiceResource.PlacementConstraintProperty[]; private readonly strategies: cloudformation.ServiceResource.PlacementStrategyProperty[]; private readonly daemon: boolean; - constructor(parent: cdk.Construct, name: string, props: EcsServiceProps) { + constructor(parent: cdk.Construct, name: string, props: Ec2ServiceProps) { if (props.daemon && props.desiredCount !== undefined) { throw new Error('Daemon mode launches one task on every instance. Don\'t supply desiredCount.'); } @@ -233,7 +233,7 @@ export class EcsService extends BaseService implements elb.ILoadBalancerTarget { /** * Validate combinations of networking arguments */ -function validateNoNetworkingProps(props: EcsServiceProps) { +function validateNoNetworkingProps(props: Ec2ServiceProps) { if (props.vpcPlacement !== undefined || props.securityGroup !== undefined) { throw new Error('vpcPlacement and securityGroup can only be used in AwsVpc networking mode'); } diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts similarity index 93% rename from packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts rename to packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts index 59bf85f223ae6..a683727b9e1af 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs/ecs-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts @@ -5,7 +5,7 @@ import { cloudformation } from '../ecs.generated'; /** * Properties to define an ECS task definition */ -export interface EcsTaskDefinitionProps extends BaseTaskDefinitionProps { +export interface Ec2TaskDefinitionProps extends BaseTaskDefinitionProps { /** * The Docker networking mode to use for the containers in the task. * @@ -26,7 +26,7 @@ export interface EcsTaskDefinitionProps extends BaseTaskDefinitionProps { /** * Define Tasks to run on an ECS cluster */ -export class EcsTaskDefinition extends BaseTaskDefinition { +export class Ec2TaskDefinition extends BaseTaskDefinition { /** * The networkmode configuration of this task */ @@ -37,7 +37,7 @@ export class EcsTaskDefinition extends BaseTaskDefinition { */ private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[]; - constructor(parent: cdk.Construct, name: string, props: EcsTaskDefinitionProps = {}) { + constructor(parent: cdk.Construct, name: string, props: Ec2TaskDefinitionProps = {}) { const networkMode = props.networkMode || NetworkMode.Bridge; super(parent, name, props, { diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index b329811d64695..0a16d2075453e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -6,9 +6,9 @@ export * from './base/scalable-task-count'; export * from './container-definition'; export * from './container-image'; -export * from './ecs/ecs-cluster'; -export * from './ecs/ecs-service'; -export * from './ecs/ecs-task-definition'; +export * from './ec2/ec2-cluster'; +export * from './ec2/ec2-service'; +export * from './ec2/ec2-task-definition'; export * from './fargate/fargate-cluster'; export * from './fargate/fargate-service'; diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts index 4ca5be7f009a2..62487d7d00ec2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts @@ -1,18 +1,18 @@ import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); import { IContainerImage } from './container-image'; -import { IEcsCluster } from './ecs/ecs-cluster'; -import { EcsService } from './ecs/ecs-service'; -import { EcsTaskDefinition } from './ecs/ecs-task-definition'; +import { IEc2Cluster } from './ec2/ec2-cluster'; +import { Ec2Service } from './ec2/ec2-service'; +import { Ec2TaskDefinition } from './ec2/ec2-task-definition'; /** - * Properties for a LoadBalancedEcsService + * Properties for a LoadBalancedEc2Service */ -export interface LoadBalancedEcsServiceProps { +export interface LoadBalancedEc2ServiceProps { /** * The cluster where your Fargate service will be deployed */ - cluster: IEcsCluster; + cluster: IEc2Cluster; /** * The image to start. @@ -59,16 +59,16 @@ export interface LoadBalancedEcsServiceProps { /** * A single task running on an ECS cluster fronted by a load balancer */ -export class LoadBalancedEcsService extends cdk.Construct { +export class LoadBalancedEc2Service extends cdk.Construct { /** * The load balancer that is fronting the ECS service */ public readonly loadBalancer: elbv2.ApplicationLoadBalancer; - constructor(parent: cdk.Construct, id: string, props: LoadBalancedEcsServiceProps) { + constructor(parent: cdk.Construct, id: string, props: LoadBalancedEc2ServiceProps) { super(parent, id); - const taskDefinition = new EcsTaskDefinition(this, 'TaskDef', {}); + const taskDefinition = new Ec2TaskDefinition(this, 'TaskDef', {}); const container = taskDefinition.addContainer('web', { image: props.image, @@ -79,7 +79,7 @@ export class LoadBalancedEcsService extends cdk.Construct { containerPort: props.containerPort || 80, }); - const service = new EcsService(this, "Service", { + const service = new Ec2Service(this, "Service", { cluster: props.cluster, taskDefinition, }); diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.asset-image.ts similarity index 73% rename from packages/@aws-cdk/aws-ecs/test/ecs/integ.asset-image.ts rename to packages/@aws-cdk/aws-ecs/test/ec2/integ.asset-image.ts index 5ad970a0ce4a8..7fc08c245f5f5 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/integ.asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.asset-image.ts @@ -7,10 +7,10 @@ const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-ecs-integ2'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); +const cluster = new ecs.Ec2Cluster(stack, 'Cluster', { vpc }); -// Instantiate Ecs Service with just cluster and image -new ecs.LoadBalancedEcsService(stack, "EcsService", { +// Instantiate Ec2 Service with just cluster and image +new ecs.LoadBalancedEc2Service(stack, "Ec2Service", { cluster, containerPort: 8000, memoryReservationMiB: 128, diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts similarity index 84% rename from packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts rename to packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts index 205372b0e36ad..62f7f86a52ea0 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts @@ -9,9 +9,9 @@ const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); +const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); -const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { +const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: NetworkMode.AwsVpc }); @@ -25,7 +25,7 @@ container.addPortMappings({ protocol: ecs.Protocol.Tcp }); -const service = new ecs.EcsService(stack, "Service", { +const service = new ecs.Ec2Service(stack, "Service", { cluster, taskDefinition, }); diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts similarity index 84% rename from packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts rename to packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts index dbd8c8e798c30..31bb39c6220ae 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts @@ -9,9 +9,9 @@ const stack = new cdk.Stack(app, 'aws-ecs-integ-ecs'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); +const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); -const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { +const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { // networkMode defaults to "bridge" // memoryMiB: '1GB', // cpu: '512' @@ -27,7 +27,7 @@ container.addPortMappings({ protocol: ecs.Protocol.Tcp }); -const service = new ecs.EcsService(stack, "Service", { +const service = new ecs.Ec2Service(stack, "Service", { cluster, taskDefinition, }); diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts similarity index 89% rename from packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts rename to packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts index ccf2801f83f11..2f5c6a4da8415 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts @@ -11,7 +11,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - new ecs.EcsCluster(stack, 'EcsCluster', { + new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc, }); @@ -34,12 +34,12 @@ export = { ImageId: "", // Should this not be the latest image ID? InstanceType: "t2.micro", IamInstanceProfile: { - Ref: "EcsClusterAutoScalingGroupInstanceProfile77D897B8" + Ref: "Ec2ClusterAutoScalingGroupInstanceProfile18B9D870" }, SecurityGroups: [ { "Fn::GetAtt": [ - "EcsClusterAutoScalingGroupInstanceSecurityGroupBFB09B50", + "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37", "GroupId" ] } @@ -51,7 +51,7 @@ export = { [ "#!/bin/bash\necho ECS_CLUSTER=", { - Ref: "EcsCluster97242B84" + Ref: "Ec2ClusterEE43E89D" }, // tslint:disable-next-line:max-line-length " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save" @@ -66,13 +66,13 @@ export = { MinSize: "0", DesiredCapacity: "1", LaunchConfigurationName: { - Ref: "EcsClusterAutoScalingGroupLaunchConfig965E00BD" + Ref: "Ec2ClusterAutoScalingGroupLaunchConfig570E562A" }, Tags: [ { Key: "Name", PropagateAtLaunch: true, - Value: "EcsCluster/AutoScalingGroup" + Value: "Ec2Cluster/AutoScalingGroup" } ], VPCZoneIdentifier: [ @@ -89,7 +89,7 @@ export = { })); expect(stack).to(haveResource("AWS::EC2::SecurityGroup", { - GroupDescription: "EcsCluster/AutoScalingGroup/InstanceSecurityGroup", + GroupDescription: "Ec2Cluster/AutoScalingGroup/InstanceSecurityGroup", SecurityGroupEgress: [ { CidrIp: "0.0.0.0/0", @@ -101,7 +101,7 @@ export = { Tags: [ { Key: "Name", - Value: "EcsCluster/AutoScalingGroup" + Value: "Ec2Cluster/AutoScalingGroup" } ], VpcId: { @@ -160,7 +160,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - new ecs.EcsCluster(stack, 'EcsCluster', { + new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc, instanceType: new InstanceType("m3.large") }); @@ -178,7 +178,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - new ecs.EcsCluster(stack, 'EcsCluster', { + new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc, size: 3 }); diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts similarity index 76% rename from packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts rename to packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts index 80545e50ed452..f51f5d21fa361 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts @@ -12,15 +12,15 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 512 }); - new ecs.EcsService(stack, "EcsService", { + new ecs.Ec2Service(stack, "Ec2Service", { cluster, taskDefinition }); @@ -28,10 +28,10 @@ export = { // THEN expect(stack).to(haveResource("AWS::ECS::Service", { TaskDefinition: { - Ref: "EcsTaskDefA3440FB6" + Ref: "Ec2TaskDef0226F28C" }, Cluster: { - Ref: "EcsCluster97242B84" + Ref: "Ec2ClusterEE43E89D" }, DeploymentConfiguration: { MaximumPercent: 200, @@ -52,12 +52,12 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); // THEN test.throws(() => { - new ecs.EcsService(stack, "EcsService", { + new ecs.Ec2Service(stack, "Ec2Service", { cluster, taskDefinition, daemon: true, @@ -72,12 +72,12 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); // THEN test.throws(() => { - new ecs.EcsService(stack, "EcsService", { + new ecs.Ec2Service(stack, "Ec2Service", { cluster, taskDefinition, }); @@ -90,15 +90,15 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 512 }); - new ecs.EcsService(stack, "EcsService", { + new ecs.Ec2Service(stack, "Ec2Service", { cluster, taskDefinition, daemon: true @@ -117,8 +117,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: NetworkMode.AwsVpc }); @@ -127,7 +127,7 @@ export = { memoryLimitMiB: 512 }); - new ecs.EcsService(stack, "EcsService", { + new ecs.Ec2Service(stack, "Ec2Service", { cluster, taskDefinition }); @@ -140,7 +140,7 @@ export = { SecurityGroups: [ { "Fn::GetAtt": [ - "EcsServiceSecurityGroup8FDFD52F", + "Ec2ServiceSecurityGroupAEC30825", "GroupId" ] } @@ -168,15 +168,15 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 512 }); - new ecs.EcsService(stack, "EcsService", { + new ecs.Ec2Service(stack, "Ec2Service", { cluster, taskDefinition, placeOnDistinctInstances: true @@ -196,15 +196,15 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 512 }); - const service = new ecs.EcsService(stack, "EcsService", { + const service = new ecs.Ec2Service(stack, "Ec2Service", { cluster, taskDefinition }); @@ -226,15 +226,15 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 512 }); - const service = new ecs.EcsService(stack, "EcsService", { + const service = new ecs.Ec2Service(stack, "Ec2Service", { cluster, taskDefinition }); @@ -256,15 +256,15 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 512 }); - const service = new ecs.EcsService(stack, "EcsService", { + const service = new ecs.Ec2Service(stack, "Ec2Service", { cluster, taskDefinition, daemon: true @@ -282,15 +282,15 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 512 }); - const service = new ecs.EcsService(stack, "EcsService", { + const service = new ecs.Ec2Service(stack, "Ec2Service", { cluster, taskDefinition }); @@ -311,15 +311,15 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 512 }); - const service = new ecs.EcsService(stack, "EcsService", { + const service = new ecs.Ec2Service(stack, "Ec2Service", { cluster, taskDefinition, daemon: true @@ -337,15 +337,15 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 512 }); - const service = new ecs.EcsService(stack, "EcsService", { + const service = new ecs.Ec2Service(stack, "Ec2Service", { cluster, taskDefinition }); @@ -367,15 +367,15 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), memoryLimitMiB: 512 }); - const service = new ecs.EcsService(stack, "EcsService", { + const service = new ecs.Ec2Service(stack, "Ec2Service", { cluster, taskDefinition, daemon: true @@ -395,14 +395,14 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'VPC'); - const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TD', { networkMode: ecs.NetworkMode.Host }); + const cluster = new ecs.Ec2Cluster(stack, 'Cluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TD', { networkMode: ecs.NetworkMode.Host }); const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image('test'), memoryLimitMiB: 1024, }); container.addPortMappings({ containerPort: 808 }); - const service = new ecs.EcsService(stack, 'Service', { cluster, taskDefinition }); + const service = new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition }); // WHEN const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); diff --git a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts similarity index 89% rename from packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts rename to packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts index ff2a0f4ed93d9..7977ca5c2f5ab 100644 --- a/packages/@aws-cdk/aws-ecs/test/ecs/test.ecs-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts @@ -10,11 +10,11 @@ export = { "with only required properties set, it correctly sets default properties"(test: Test) { // GIVEN const stack = new cdk.Stack(); - new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); // THEN expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { - Family: "EcsTaskDef", + Family: "Ec2TaskDef", ContainerDefinitions: [], PlacementConstraints: [], Volumes: [], @@ -29,7 +29,7 @@ export = { "correctly sets network mode"(test: Test) { // GIVEN const stack = new cdk.Stack(); - new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: ecs.NetworkMode.AwsVpc }); @@ -45,7 +45,7 @@ export = { // GIVEN const stack = new cdk.Stack(); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef'); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); const container = taskDefinition.addContainer("web", { image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), @@ -59,7 +59,7 @@ export = { // THEN expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { - Family: "EcsTaskDef", + Family: "Ec2TaskDef", ContainerDefinitions: [{ Essential: true, Memory: 512, @@ -99,7 +99,7 @@ export = { }; // Adding volumes via props is a bit clunky - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { volumes: [volume] }); @@ -117,7 +117,7 @@ export = { // THEN expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { - Family: "EcsTaskDef", + Family: "Ec2TaskDef", ContainerDefinitions: [{ MountPoints: [ { @@ -141,7 +141,7 @@ export = { "correctly sets placement constraints"(test: Test) { // GIVEN const stack = new cdk.Stack(); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { placementConstraints: [{ expression: "attribute:ecs.instance-type =~ t2.*", type: ecs.PlacementConstraintType.MemberOf @@ -169,7 +169,7 @@ export = { // "correctly sets taskRole"(test: Test) { // // GIVEN // const stack = new cdk.Stack(); - // const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', { + // const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { // taskRole: new iam.Role(this, 'TaskRole', { // assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), // }) @@ -191,7 +191,7 @@ export = { // "correctly sets taskExecutionRole if containerDef uses ECR"(test: Test) { // // GIVEN // const stack = new cdk.Stack(); - // const taskDefinition = new ecs.EcsTaskDefinition(stack, 'EcsTaskDef', {}); + // const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', {}); // const container = taskDefinition.addContainer("web", { // image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), // memoryLimitMiB: 512 // add validation? diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index dba82d0b35982..4640bd8f5a9cc 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -10,7 +10,7 @@ export = { "Host port should be the same as container port"(test: Test) { // GIVEN const stack = new cdk.Stack(); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: ecs.NetworkMode.AwsVpc, }); @@ -31,7 +31,7 @@ export = { "Host port can be empty "(test: Test) { // GIVEN const stack = new cdk.Stack(); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: ecs.NetworkMode.AwsVpc, }); @@ -55,7 +55,7 @@ export = { "Host port can be empty "(test: Test) { // GIVEN const stack = new cdk.Stack(); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: ecs.NetworkMode.AwsVpc, }); @@ -76,7 +76,7 @@ export = { "Host port should not be lower than 1024"(test: Test) { // GIVEN const stack = new cdk.Stack(); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: ecs.NetworkMode.AwsVpc, }); @@ -111,7 +111,7 @@ export = { "Ingress port should be the same as container port"(test: Test) { // GIVEN const stack = new cdk.Stack(); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: ecs.NetworkMode.AwsVpc, }); @@ -136,7 +136,7 @@ export = { "Ingress port should be the same as container port"(test: Test) { // GIVEN const stack = new cdk.Stack(); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: ecs.NetworkMode.Host, }); @@ -161,7 +161,7 @@ export = { "Ingress port should be the same as host port if supplied"(test: Test) { // GIVEN const stack = new cdk.Stack(); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: ecs.NetworkMode.Bridge, }); @@ -185,7 +185,7 @@ export = { "Ingress port should be 0 if not supplied"(test: Test) { // GIVEN const stack = new cdk.Stack(); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef', { + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: ecs.NetworkMode.Bridge, }); @@ -211,7 +211,7 @@ export = { 'can add AWS logging to container definition'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const taskDefinition = new ecs.EcsTaskDefinition(stack, 'TaskDef'); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); // WHEN taskDefinition.addContainer('cont', { diff --git a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts index 125cc5736f771..57998d842358d 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts @@ -9,10 +9,10 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'VPC'); - const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); + const cluster = new ecs.Ec2Cluster(stack, 'Cluster', { vpc }); // WHEN - new ecs.LoadBalancedEcsService(stack, 'Service', { + new ecs.LoadBalancedEc2Service(stack, 'Service', { cluster, memoryLimitMiB: 1024, image: ecs.DockerHub.image('test') From 360d42f70372630d3c2dc716984eede7079a7e0b Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 31 Oct 2018 23:01:14 -0700 Subject: [PATCH 110/140] Add test for adding Ulimit --- .../aws-ecs/test/ec2/test.ec2-task-definition.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts index 7977ca5c2f5ab..6cb9e9f2aab01 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts @@ -57,6 +57,12 @@ export = { containerPort: 3000 }); + container.addUlimits({ + hardLimit: 128, + name: ecs.UlimitName.Rss, + softLimit: 128 + }); + // THEN expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { Family: "Ec2TaskDef", @@ -80,7 +86,11 @@ export = { HostPort: 0, Protocol: Protocol.Tcp }], - Ulimits: [], + Ulimits: [{ + HardLimit: 128, + Name: "rss", + SoftLimit: 128 + }], VolumesFrom: [] }], })); From 8c76aa460f9095128d06e79bc81b0bad2f28620f Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 31 Oct 2018 23:46:07 -0700 Subject: [PATCH 111/140] Add test for adding links --- .../aws-ecs/lib/container-definition.ts | 6 ++ .../aws-ecs/test/test.container-definition.ts | 86 +++++++++++++++---- 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index bd933de9de04f..185f4dc8f8e88 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -233,8 +233,14 @@ export class ContainerDefinition extends cdk.Construct { /** * Add a link from this container to a different container + * The link parameter allows containers to communicate with each other without the need for port mappings. + * Only supported if the network mode of a task definition is set to bridge. + * Warning: The --link flag is a legacy feature of Docker. It may eventually be removed. */ public addLink(container: ContainerDefinition, alias?: string) { + if (this.taskDefinition.networkMode !== NetworkMode.Bridge) { + throw new Error(`You must use network mode Bridge to add container links.`); + } if (alias !== undefined) { this.links.push(`${container.id}:${alias}`); } else { diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index 4640bd8f5a9cc..a4599a7c3676b 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -9,7 +9,7 @@ export = { "With network mode AwsVpc": { "Host port should be the same as container port"(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new cdk.Stack(); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: ecs.NetworkMode.AwsVpc, }); @@ -18,6 +18,7 @@ export = { image: ecs.DockerHub.image("/aws/aws-example-app"), memoryLimitMiB: 2048, }); + // THEN test.throws(() => { container.addPortMappings({ @@ -25,12 +26,13 @@ export = { hostPort: 8081 }); }); + test.done(); }, "Host port can be empty "(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new cdk.Stack(); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: ecs.NetworkMode.AwsVpc, }); @@ -39,30 +41,53 @@ export = { image: ecs.DockerHub.image("/aws/aws-example-app"), memoryLimitMiB: 2048, }); + // WHEN container.addPortMappings({ containerPort: 8080, }); - // THEN no excpetion raised + // THEN no exception raised test.done(); }, }, + "With network mode Host ": { "Host port should be the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Host, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // THEN + test.throws(() => { + container.addPortMappings({ + containerPort: 8080, + hostPort: 8081 + }); + }); + test.done(); }, + "Host port can be empty "(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new cdk.Stack(); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { - networkMode: ecs.NetworkMode.AwsVpc, + networkMode: ecs.NetworkMode.Host, }); const container = taskDefinition.addContainer("Container", { image: ecs.DockerHub.image("/aws/aws-example-app"), memoryLimitMiB: 2048, }); + // WHEN container.addPortMappings({ containerPort: 8080, @@ -71,13 +96,12 @@ export = { // THEN no exception raised test.done(); }, - }, - "With network mode Bridge": { - "Host port should not be lower than 1024"(test: Test) { + + "errors when adding links"(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new cdk.Stack(); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { - networkMode: ecs.NetworkMode.AwsVpc, + networkMode: ecs.NetworkMode.Host, }); const container = taskDefinition.addContainer("Container", { @@ -85,17 +109,45 @@ export = { memoryLimitMiB: 2048, }); + const logger = taskDefinition.addContainer("LoggingContainer", { + image: ecs.DockerHub.image("myLogger"), + memoryLimitMiB: 1024, + }); + // THEN test.throws(() => { - container.addPortMappings({ - containerPort: 8080, - hostPort: 1, - }); + container.addLink(logger); }); + test.done(); }, }, + "With network mode Bridge": { + "allows adding links"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.Bridge, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.DockerHub.image("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + const logger = taskDefinition.addContainer("LoggingContainer", { + image: ecs.DockerHub.image("myLogger"), + memoryLimitMiB: 1024, + }); + + // THEN + container.addLink(logger); + + test.done(); + }, + } + // "With health check": { // "healthCheck.command is a single string"(test: Test) { // const stack = new cdk.Stack(); @@ -157,6 +209,7 @@ export = { test.done(); }, }, + "With network mode Bridge": { "Ingress port should be the same as host port if supplied"(test: Test) { // GIVEN @@ -182,6 +235,7 @@ export = { test.equal(actual, expected); test.done(); }, + "Ingress port should be 0 if not supplied"(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -210,7 +264,7 @@ export = { 'can add AWS logging to container definition'(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new cdk.Stack(); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); // WHEN @@ -238,4 +292,4 @@ export = { test.done(); }, -}; +}; \ No newline at end of file From 45a82c880696e617658b9bc49b410431330814f6 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 1 Nov 2018 10:24:40 +0100 Subject: [PATCH 112/140] Fix asset tests --- packages/@aws-cdk/aws-ecs/lib/asset-image.ts | 114 +-------- .../aws-ecs/test/ec2/integ.asset-image.ts | 22 -- .../integ.asset-image.expected.json | 0 .../test/integ.lb-awsvpc-nw.expected.json | 242 ++++++++++++------ .../@aws-cdk/aws-ecs/test/test.asset-image.ts | 30 +++ 5 files changed, 200 insertions(+), 208 deletions(-) delete mode 100644 packages/@aws-cdk/aws-ecs/test/ec2/integ.asset-image.ts rename packages/@aws-cdk/aws-ecs/test/{ => fargate}/integ.asset-image.expected.json (100%) create mode 100644 packages/@aws-cdk/aws-ecs/test/test.asset-image.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/asset-image.ts b/packages/@aws-cdk/aws-ecs/lib/asset-image.ts index 0bbdd44de95ae..604d5fb65d988 100644 --- a/packages/@aws-cdk/aws-ecs/lib/asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/lib/asset-image.ts @@ -100,8 +100,8 @@ class AdoptRegistry extends cdk.Construct { const fn = new lambda.SingletonFunction(this, 'Function', { runtime: lambda.Runtime.NodeJS810, lambdaPurpose: 'AdoptEcrRegistry', - handler: 'index.handler', - code: lambda.Code.inline(`exports.handler = ${trivialMinify(adoptRegistryHandler.toString())}`), + handler: 'handler.handler', + code: lambda.Code.asset(path.join(__dirname, 'adopt-registry')), uuid: 'dbc60def-c595-44bc-aa5c-28c95d68f62c', timeout: 300 }); @@ -120,113 +120,3 @@ class AdoptRegistry extends cdk.Construct { this.repositoryUri = resource.getAtt('RepositoryUri').toString(); } } - -// tslint:disable:no-console -async function adoptRegistryHandler(event: any, context: any) { - try { - const AWS = require('aws-sdk'); - const ecr = new AWS.ECR(); - - console.log(JSON.stringify(event)); - - const markerStatement = { - Sid: event.StackId, - Effect: "Deny", - Action: "OwnedBy:CDKStack", - Principal: "*" - }; - - function repoName(props: any) { - return props.RepositoryArn.split('/').slice(1).join('/'); - } - - // The repository must already exist - async function getAdopter(name: string): Promise { - try { - const policyResponse = await ecr.getRepositoryPolicy({ repositoryName: name }).promise(); - const policy = JSON.parse(policyResponse.policyText); - // Search the policy for an adopter marker - return (policy.Statement || []).find((x: any) => x.Action === markerStatement.Action) || {}; - } catch (e) { - if (e.code !== 'RepositoryPolicyNotFoundException') { throw e; } - return {}; - } - } - - const repo = repoName(event.ResourceProperties); - const adopter = await getAdopter(repo); - if (event.RequestType === 'Delete') { - if (adopter.Sid !== markerStatement.Sid) { - throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); - } - console.log('Deleting', repo); - const ids = (await ecr.listImages({ repositoryName: repo }).promise()).imageIds; - try { - await ecr.batchDeleteImage({ repositoryName: repo, imageIds: ids }).promise(); - await ecr.deleteRepository({ repositoryName: repo }).promise(); - } catch (e) { - if (e.code !== 'RepositoryPolicyNotFoundException') { throw e; } - } - } - - if (event.RequestType === 'Create' || event.RequestType === 'Update') { - if (adopter.Sid !== undefined && adopter.Sid !== markerStatement.Sid) { - throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); - } - console.log('Adopting', repo); - await ecr.setRepositoryPolicy({ repositoryName: repo, policyText: JSON.stringify({ - Version: '2008-10-17', - Statement: [markerStatement] - }) }).promise(); - } - - const arn = event.ResourceProperties.RepositoryArn.split(':'); - await respond("SUCCESS", "OK", repo, { - RepositoryUri: `${arn[4]}.dkr.ecr.${arn[3]}.amazonaws.com/${repoName(event.ResourceProperties)}` - }); - } catch (e) { - console.log(e); - await respond("FAILED", e.message, context.logStreamName, {}); - } - - function respond(responseStatus: string, reason: string, physId: string, data: any) { - const responseBody = JSON.stringify({ - Status: responseStatus, - Reason: reason, - PhysicalResourceId: physId, - StackId: event.StackId, - RequestId: event.RequestId, - LogicalResourceId: event.LogicalResourceId, - NoEcho: false, - Data: data - }); - - console.log('Responding', JSON.stringify(responseBody)); - - const parsedUrl = require('url').parse(event.ResponseURL); - const requestOptions = { - hostname: parsedUrl.hostname, - path: parsedUrl.path, - method: "PUT", - headers: { "content-type": "", "content-length": responseBody.length } - }; - - return new Promise((resolve, reject) => { - try { - const request = require('https').request(requestOptions, resolve); - request.on("error", reject); - request.write(responseBody); - request.end(); - } catch (e) { - reject(e); - } - }); - } -} - -/** - * Trivial minification by changing TypeScript's 4-space indentation to 1-space - */ -function trivialMinify(s: string) { - return s.replace(/^ {4}/mg, ' '); -} diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.asset-image.ts deleted file mode 100644 index 7fc08c245f5f5..0000000000000 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.asset-image.ts +++ /dev/null @@ -1,22 +0,0 @@ -import ec2 = require('@aws-cdk/aws-ec2'); -import cdk = require('@aws-cdk/cdk'); -import path = require('path'); -import ecs = require('../../lib'); - -const app = new cdk.App(); -const stack = new cdk.Stack(app, 'aws-ecs-integ2'); -const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); - -const cluster = new ecs.Ec2Cluster(stack, 'Cluster', { vpc }); - -// Instantiate Ec2 Service with just cluster and image -new ecs.LoadBalancedEc2Service(stack, "Ec2Service", { - cluster, - containerPort: 8000, - memoryReservationMiB: 128, - image: new ecs.AssetImage(stack, 'Image', { - directory: path.join(__dirname, '..', 'demo-image') - }) -}); - -app.run(); diff --git a/packages/@aws-cdk/aws-ecs/test/integ.asset-image.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json similarity index 100% rename from packages/@aws-cdk/aws-ecs/test/integ.asset-image.expected.json rename to packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json diff --git a/packages/@aws-cdk/aws-ecs/test/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/integ.lb-awsvpc-nw.expected.json index d4f7ba85f8c29..233d2d162f2d1 100644 --- a/packages/@aws-cdk/aws-ecs/test/integ.lb-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/integ.lb-awsvpc-nw.expected.json @@ -305,9 +305,164 @@ } } }, - "FargateCluster7CCD5F93": { + "Ec2ClusterEE43E89D": { "Type": "AWS::ECS::Cluster" }, + "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ/Ec2Cluster/AutoScalingGroup/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Ec2Cluster/AutoScalingGroup" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "Ec2ClusterAutoScalingGroupInstanceRoleDefaultPolicyD987CC12": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Ec2ClusterAutoScalingGroupInstanceRoleDefaultPolicyD987CC12", + "Roles": [ + { + "Ref": "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C" + } + ] + } + }, + "Ec2ClusterAutoScalingGroupInstanceProfile18B9D870": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C" + } + ] + } + }, + "Ec2ClusterAutoScalingGroupLaunchConfig570E562A": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": "ami-1234", + "InstanceType": "t2.micro", + "IamInstanceProfile": { + "Ref": "Ec2ClusterAutoScalingGroupInstanceProfile18B9D870" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "Ref": "Ec2ClusterEE43E89D" + }, + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save" + ] + ] + } + } + }, + "DependsOn": [ + "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C", + "Ec2ClusterAutoScalingGroupInstanceRoleDefaultPolicyD987CC12" + ] + }, + "Ec2ClusterAutoScalingGroupASGC290B9EA": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "0", + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "Ec2ClusterAutoScalingGroupLaunchConfig570E562A" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-ecs-integ/Ec2Cluster/AutoScalingGroup" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, "TaskDefTaskRole1EDB4A67": { "Type": "AWS::IAM::Role", "Properties": { @@ -341,6 +496,7 @@ "Devices": [], "Tmpfs": [] }, + "Memory": 256, "MountPoints": [], "Name": "web", "PortMappings": [ @@ -353,12 +509,11 @@ "VolumesFrom": [] } ], - "Cpu": "512", "Family": "awsecsintegTaskDef6FDFB69A", - "Memory": "1GB", "NetworkMode": "awsvpc", + "PlacementConstraints": [], "RequiresCompatibilities": [ - "FARGATE" + "EC2" ], "TaskRoleArn": { "Fn::GetAtt": [ @@ -376,20 +531,20 @@ "Ref": "TaskDef54694570" }, "Cluster": { - "Ref": "FargateCluster7CCD5F93" + "Ref": "Ec2ClusterEE43E89D" }, "DeploymentConfiguration": { "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, "DesiredCount": 1, - "LaunchType": "FARGATE", + "LaunchType": "EC2", "LoadBalancers": [ { "ContainerName": "web", "ContainerPort": 80, "TargetGroupArn": { - "Ref": "LBPublicListenerFargateGroup5EE2FBAF" + "Ref": "LBPublicListenerECSGroupD6A32205" } } ], @@ -413,7 +568,10 @@ } ] } - } + }, + "PlacementConstraints": [], + "PlacementStrategies": [], + "SchedulingStrategy": "REPLICA" }, "DependsOn": [ "LBPublicListener6E1F3D94" @@ -457,70 +615,6 @@ "ToPort": 80 } }, - "ServiceTaskCountTarget23E25614": { - "Type": "AWS::ApplicationAutoScaling::ScalableTarget", - "Properties": { - "MaxCapacity": 10, - "MinCapacity": 1, - "ResourceId": { - "Fn::Join": [ - "", - [ - "service/", - { - "Ref": "FargateCluster7CCD5F93" - }, - "/", - { - "Fn::GetAtt": [ - "ServiceD69D759B", - "Name" - ] - } - ] - ] - }, - "RoleARN": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService" - ] - ] - }, - "ScalableDimension": "ecs:service:DesiredCount", - "ServiceNamespace": "ecs", - "ScheduledActions": [] - } - }, - "ServiceTaskCountTargetReasonableCpu4174EFCE": { - "Type": "AWS::ApplicationAutoScaling::ScalingPolicy", - "Properties": { - "PolicyName": "awsecsintegServiceTaskCountTargetReasonableCpuDB6AEA73", - "PolicyType": "TargetTrackingScaling", - "ScalingTargetId": { - "Ref": "ServiceTaskCountTarget23E25614" - }, - "TargetTrackingScalingPolicyConfiguration": { - "PredefinedMetricSpecification": { - "PredefinedMetricType": "ECSServiceAverageCPUUtilization" - }, - "TargetValue": 10 - } - } - }, "LB8A12904C": { "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", "Properties": { @@ -591,7 +685,7 @@ "DefaultActions": [ { "TargetGroupArn": { - "Ref": "LBPublicListenerFargateGroup5EE2FBAF" + "Ref": "LBPublicListenerECSGroupD6A32205" }, "Type": "forward" } @@ -604,7 +698,7 @@ "Certificates": [] } }, - "LBPublicListenerFargateGroup5EE2FBAF": { + "LBPublicListenerECSGroupD6A32205": { "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", "Properties": { "Port": 80, diff --git a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts new file mode 100644 index 0000000000000..67e716f1bb297 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts @@ -0,0 +1,30 @@ +import { expect, MatchStyle } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../lib'); + +export = { + 'test instantiating Asset Image'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ecs.AssetImage(stack, 'Image', { + directory: '.' + }); + + // THEN + expect(stack).toMatch({ + ImageRepositoryC2BE7AD4: { + Type: "String", + Description: "Repository ARN for asset \"Image\"" + }, + ImageTagE17D8A6B: { + Type: "String", + Description: "Tag for asset \"Image\"" + }, + }, MatchStyle.SUPERSET); + + test.done(); + } +}; From 013a52ca837110568fc71d07571a822cf7704398 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 1 Nov 2018 10:42:56 +0100 Subject: [PATCH 113/140] Don't forget to unignore bundled .js file --- packages/@aws-cdk/aws-ecs/.gitignore | 3 +- packages/@aws-cdk/aws-ecs/.npmignore | 1 + .../aws-ecs/lib/adopt-registry/handler.js | 101 ++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/adopt-registry/handler.js diff --git a/packages/@aws-cdk/aws-ecs/.gitignore b/packages/@aws-cdk/aws-ecs/.gitignore index 7d1f7e309d0c3..6d377d90bb034 100644 --- a/packages/@aws-cdk/aws-ecs/.gitignore +++ b/packages/@aws-cdk/aws-ecs/.gitignore @@ -12,4 +12,5 @@ dist .nyc_output coverage .nycrc -.LAST_PACKAGE \ No newline at end of file +.LAST_PACKAGE +!lib/adopt-registry/* diff --git a/packages/@aws-cdk/aws-ecs/.npmignore b/packages/@aws-cdk/aws-ecs/.npmignore index e511c5acc268d..a015cfc1dcb16 100644 --- a/packages/@aws-cdk/aws-ecs/.npmignore +++ b/packages/@aws-cdk/aws-ecs/.npmignore @@ -12,3 +12,4 @@ dist # Include .jsii !.jsii +!lib/adopt-registry/* diff --git a/packages/@aws-cdk/aws-ecs/lib/adopt-registry/handler.js b/packages/@aws-cdk/aws-ecs/lib/adopt-registry/handler.js new file mode 100644 index 0000000000000..d96d3614b25bb --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/adopt-registry/handler.js @@ -0,0 +1,101 @@ +const AWS = require('aws-sdk'); +const ecr = new AWS.ECR(); + +exports.handler = async function adoptRegistryHandler(event, context) { + try { + console.log(JSON.stringify(event)); + + const markerStatement = { + Sid: event.StackId, + Effect: "Deny", + Action: "OwnedBy:CDKStack", + Principal: "*" + }; + + function repoName(props) { + return props.RepositoryArn.split('/').slice(1).join('/'); + } + + // The repository must already exist + async function getAdopter(name): Promise { + try { + const policyResponse = await ecr.getRepositoryPolicy({ repositoryName: name }).promise(); + const policy = JSON.parse(policyResponse.policyText); + // Search the policy for an adopter marker + return (policy.Statement || []).find((x) => x.Action === markerStatement.Action) || {}; + } catch (e) { + if (e.code !== 'RepositoryPolicyNotFoundException') { throw e; } + return {}; + } + } + + const repo = repoName(event.ResourceProperties); + const adopter = await getAdopter(repo); + if (event.RequestType === 'Delete') { + if (adopter.Sid !== markerStatement.Sid) { + throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); + } + console.log('Deleting', repo); + const ids = (await ecr.listImages({ repositoryName: repo }).promise()).imageIds; + try { + await ecr.batchDeleteImage({ repositoryName: repo, imageIds: ids }).promise(); + await ecr.deleteRepository({ repositoryName: repo }).promise(); + } catch (e) { + if (e.code !== 'RepositoryPolicyNotFoundException') { throw e; } + } + } + + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + if (adopter.Sid !== undefined && adopter.Sid !== markerStatement.Sid) { + throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); + } + console.log('Adopting', repo); + await ecr.setRepositoryPolicy({ repositoryName: repo, policyText: JSON.stringify({ + Version: '2008-10-17', + Statement: [markerStatement] + }) }).promise(); + } + + const arn = event.ResourceProperties.RepositoryArn.split(':'); + await respond("SUCCESS", "OK", repo, { + RepositoryUri: `${arn[4]}.dkr.ecr.${arn[3]}.amazonaws.com/${repoName(event.ResourceProperties)}` + }); + } catch (e) { + console.log(e); + await respond("FAILED", e.message, context.logStreamName, {}); + } + + function respond(responseStatus, reason, physId, data) { + const responseBody = JSON.stringify({ + Status: responseStatus, + Reason: reason, + PhysicalResourceId: physId, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + NoEcho: false, + Data: data + }); + + console.log('Responding', JSON.stringify(responseBody)); + + const parsedUrl = require('url').parse(event.ResponseURL); + const requestOptions = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: "PUT", + headers: { "content-type": "", "content-length": responseBody.length } + }; + + return new Promise((resolve, reject) => { + try { + const request = require('https').request(requestOptions, resolve); + request.on("error", reject); + request.write(responseBody); + request.end(); + } catch (e) { + reject(e); + } + }); + } +} From ce083db03bcc5e985fd991a9f03baddcab5a3c2f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 1 Nov 2018 14:06:51 +0100 Subject: [PATCH 114/140] Review comments --- .gitignore | 1 - examples/cdk-examples-typescript/.gitignore | 3 +- .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 19 ++++++++ packages/@aws-cdk/aws-ecs/.gitignore | 2 +- packages/@aws-cdk/aws-ecs/.npmignore | 2 +- packages/@aws-cdk/aws-ecs/README.md | 29 +++++++++-- .../handler.js | 0 packages/@aws-cdk/aws-ecs/lib/asset-image.ts | 33 ++++++++----- .../@aws-cdk/aws-ecs/lib/base/base-cluster.ts | 2 +- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 48 +------------------ .../aws-ecs/lib/fargate/fargate-service.ts | 44 +++++++++++++++++ 11 files changed, 117 insertions(+), 66 deletions(-) rename packages/@aws-cdk/aws-ecs/lib/{adopt-registry => adopt-repository}/handler.js (100%) diff --git a/.gitignore b/.gitignore index bd75b435a1ac4..5a03c2b774932 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,3 @@ coverage .nyc_output .LAST_BUILD *.swp -./examples/cdk-examples-typescript/hello-cdk-ecs/cdk.json diff --git a/examples/cdk-examples-typescript/.gitignore b/examples/cdk-examples-typescript/.gitignore index f191b23ce81b5..5a60dd5267c00 100644 --- a/examples/cdk-examples-typescript/.gitignore +++ b/examples/cdk-examples-typescript/.gitignore @@ -1 +1,2 @@ -.LAST_BUILD \ No newline at end of file +.LAST_BUILD +hello-cdk-ecs/cdk.json diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index 9b5985d38aaed..db048ed40a44f 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -52,6 +52,25 @@ export abstract class RepositoryRef extends cdk.Construct { public getImage(tag: string = "latest"): ecs.IContainerImage { return new EcrImage(this, tag); } + + /** + * Grant the given principal identity permissions to perform the actions on this repository + */ + public grant(identity?: iam.IPrincipal, ...actions: string[]) { + if (!identity) { + return; + } + identity.addToPolicy(new iam.PolicyStatement() + .addResource(this.repositoryArn) + .addActions(...actions)); + } + + /** + * Grant the given identity permissions to use the images in this repository + */ + public grantUseImage(identity?: iam.IPrincipal) { + this.grant(identity, "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage"); + } } export interface RepositoryRefProps { diff --git a/packages/@aws-cdk/aws-ecs/.gitignore b/packages/@aws-cdk/aws-ecs/.gitignore index 6d377d90bb034..718668bd257a6 100644 --- a/packages/@aws-cdk/aws-ecs/.gitignore +++ b/packages/@aws-cdk/aws-ecs/.gitignore @@ -13,4 +13,4 @@ dist coverage .nycrc .LAST_PACKAGE -!lib/adopt-registry/* +!lib/adopt-repository/* diff --git a/packages/@aws-cdk/aws-ecs/.npmignore b/packages/@aws-cdk/aws-ecs/.npmignore index a015cfc1dcb16..1ceea742e2019 100644 --- a/packages/@aws-cdk/aws-ecs/.npmignore +++ b/packages/@aws-cdk/aws-ecs/.npmignore @@ -12,4 +12,4 @@ dist # Include .jsii !.jsii -!lib/adopt-registry/* +!lib/adopt-repository/* diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 893ebf1d15c7c..1f30516772059 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -22,18 +22,36 @@ const ecsService = new ecs.LoadBalancedEc2Service(this, 'Service', { ### Fargate vs ECS There are two sets of constructs in this library; one to run tasks on ECS and -one to run Tasks on fargate. +one to run Tasks on Fargate. - Use the `Ec2Cluster`, `Ec2TaskDefinition` and `Ec2Service` constructs to run tasks on EC2 instances running in your account. - Use the `FargateCluster`, `FargateTaskDefinition` and `FargateService` constructs to run tasks on instances that are managed for you by AWS. +Here are the main differences: + +- **EC2**: instances are under your control. Complete control of task to host + allocation. Required to specify at least a memory reseration or limit for + every container. Can use Host, Bridge and AwsVpc networking modes. Can attach + Classic Load Balancer. Can share volumes between container and host. +- **Fargate**: tasks run on AWS-managed instances, AWS manages task to host + allocation for you. Requires specification of memory and cpu sizes at the + taskdefinition level. Only supports AwsVpc networking modes and + Application/Network Load Balancers. Only the AWS log driver is supported. + Many host features are not supported such as adding kernel capabilities + and mounting host devices/volumes inside the container. + +For more information on EC2 vs Fargate and networking see the AWS Documentation: +[AWS Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html) and +[Task Networking](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html). + + ## Cluster An `Ec2Cluster` or `FargateCluster` defines the infrastructure to run your tasks on. If you create an ECS cluster, an AutoScalingGroup of EC2 instances -running the latest ECS Optimized AMI will implicitly be created for you. +running the latest ECS Optimized AMI will automatically be created for you. You can run many tasks on a single cluster. @@ -87,7 +105,7 @@ automatically be restarted. ```ts const taskDefinition; -const service = new ecs.Ec2Service(this, 'Service', { +const service = new ecs.FargateService(this, 'Service', { cluster, taskDefinition, desiredCount: 5 @@ -139,4 +157,7 @@ your EC2 instances might fill up as your number of Tasks goes up. To avoid placement errors, you will want to configure AutoScaling for your EC2 instance group so that your instance count scales with demand. -TO BE IMPLEMENTED \ No newline at end of file +## Roadmap + +- [ ] Instance AutoScaling +- [ ] Service Discovery Integration diff --git a/packages/@aws-cdk/aws-ecs/lib/adopt-registry/handler.js b/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js similarity index 100% rename from packages/@aws-cdk/aws-ecs/lib/adopt-registry/handler.js rename to packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js diff --git a/packages/@aws-cdk/aws-ecs/lib/asset-image.ts b/packages/@aws-cdk/aws-ecs/lib/asset-image.ts index 604d5fb65d988..d1f1fd22e739a 100644 --- a/packages/@aws-cdk/aws-ecs/lib/asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/lib/asset-image.ts @@ -16,7 +16,7 @@ export interface AssetImageProps { } /** - * An image that will be built at deployment time + * An image that will be built at synthesis time */ export class AssetImage extends cdk.Construct implements IContainerImage { /** @@ -42,6 +42,9 @@ export class AssetImage extends cdk.Construct implements IContainerImage { if (!fs.existsSync(this.directory)) { throw new Error(`Cannot find image directory at ${this.directory}`); } + if (!fs.existsSync(path.join(this.directory, 'Dockerfile'))) { + throw new Error(`No 'Dockerfile' found in ${this.directory}`); + } const repositoryParameter = new cdk.Parameter(this, 'Repository', { type: 'String', @@ -65,8 +68,13 @@ export class AssetImage extends cdk.Construct implements IContainerImage { this.repositoryArn = repositoryParameter.value.toString(); - // Require that repository adoption happens first - const adopted = new AdoptRegistry(this, 'AdoptRegistry', { repositoryArn: this.repositoryArn }); + // Require that repository adoption happens first, so we route the + // input ARN into the Custom Resource and then get the URI which we use to + // refer to the image FROM the Custom Resource. + // + // If adoption fails (because the repository might be twice-adopted), we + // haven't already started using the image. + const adopted = new AdoptRepository(this, 'doptRegistry', { repositoryArn: this.repositoryArn }); this.imageName = `${adopted.repositoryUri}:${tagParameter.value}`; } @@ -82,33 +90,36 @@ export class AssetImage extends cdk.Construct implements IContainerImage { } } -interface AdoptRegistryProps { +interface AdoptRepositoryProps { repositoryArn: string; } /** - * Custom Resource which will adopt the registry used for the locally built image into the stack. + * Custom Resource which will adopt the repository used for the locally built image into the stack. * - * This is so we can clean it up when the stack gets deleted. + * Since the repository is not created by the stack (but by the CDK toolkit), + * adopting will make the repository "owned" by the stack. It will be cleaned + * up when the stack gets deleted, to avoid leaving orphaned repositories on stack + * cleanup. */ -class AdoptRegistry extends cdk.Construct { +class AdoptRepository extends cdk.Construct { public readonly repositoryUri: string; - constructor(parent: cdk.Construct, id: string, props: AdoptRegistryProps) { + constructor(parent: cdk.Construct, id: string, props: AdoptRepositoryProps) { super(parent, id); const fn = new lambda.SingletonFunction(this, 'Function', { runtime: lambda.Runtime.NodeJS810, - lambdaPurpose: 'AdoptEcrRegistry', + lambdaPurpose: 'AdoptEcrRepository', handler: 'handler.handler', - code: lambda.Code.asset(path.join(__dirname, 'adopt-registry')), + code: lambda.Code.asset(path.join(__dirname, 'adopt-repository')), uuid: 'dbc60def-c595-44bc-aa5c-28c95d68f62c', timeout: 300 }); fn.addToRolePolicy(new iam.PolicyStatement() .addActions('ecr:GetRepositoryPolicy', 'ecr:SetRepositoryPolicy', 'ecr:DeleteRepository', 'ecr:ListImages', 'ecr:BatchDeleteImage') - .addAllResources()); + .addResource(props.repositoryArn)); const resource = new cfn.CustomResource(this, 'Resource', { lambdaProvider: fn, diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts index 823ed7ceae715..614b880980288 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts @@ -46,7 +46,7 @@ export abstract class BaseCluster extends cdk.Construct { this.vpc = props.vpc; this.clusterArn = cluster.clusterArn; - this.clusterName = cluster.ref; + this.clusterName = cluster.clusterName; } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 659046290af4d..9fc97a7a5becc 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -50,16 +50,6 @@ export interface BaseServiceProps { * @default ??? FIXME */ healthCheckGracePeriodSeconds?: number; - - /** - * Fargate platform version to run this service on - * - * Unless you have specific compatibility requirements, you don't need to - * specify this. - * - * @default Latest - */ - platformVersion?: FargatePlatformVersion; } /** @@ -74,7 +64,7 @@ export abstract class BaseService extends cdk.Construct public readonly dependencyElements: cdk.IDependable[]; /** - * Manage allowed network traffic for this construct + * Manage allowed network traffic for this service */ public abstract readonly connections: ec2.Connections; @@ -113,7 +103,6 @@ export abstract class BaseService extends cdk.Construct }, /* role: never specified, supplanted by Service Linked Role */ networkConfiguration: new cdk.Token(() => this.networkConfiguration), - platformVersion: props.platformVersion, ...additionalProps }); this.serviceArn = this.resource.serviceArn; @@ -247,37 +236,4 @@ export abstract class BaseService extends cdk.Construct /** * The port range to open up for dynamic port mapping */ -const EPHEMERAL_PORT_RANGE = new ec2.TcpPortRange(32768, 65535); - -/** - * Fargate platform version - * - * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html - */ -export enum FargatePlatformVersion { - /** - * The latest, recommended platform version - */ - Latest = 'LATEST', - - /** - * Version 1.2 - * - * Supports private registries. - */ - Version12 = '1.2.0', - - /** - * Version 1.1.0 - * - * Supports task metadata, health checks, service discovery. - */ - Version11 = '1.1.0', - - /** - * Initial release - * - * Based on Amazon Linux 2017.09. - */ - Version10 = '1.0.0', -} \ No newline at end of file +const EPHEMERAL_PORT_RANGE = new ec2.TcpPortRange(32768, 65535); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index d3ea11ef4a659..64e10856bafde 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -39,6 +39,16 @@ export interface FargateServiceProps extends BaseServiceProps { * @default A new security group is created */ securityGroup?: ec2.SecurityGroupRef; + + /** + * Fargate platform version to run this service on + * + * Unless you have specific compatibility requirements, you don't need to + * specify this. + * + * @default Latest + */ + platformVersion?: FargatePlatformVersion; } /** @@ -61,6 +71,7 @@ export class FargateService extends BaseService { cluster: props.cluster.clusterName, taskDefinition: props.taskDefinition.taskDefinitionArn, launchType: 'FARGATE', + platformVersion: props.platformVersion, }, props.cluster.clusterName); this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcPlacement, props.securityGroup); @@ -74,3 +85,36 @@ export class FargateService extends BaseService { this.taskDef = props.taskDefinition; } } + +/** + * Fargate platform version + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html + */ +export enum FargatePlatformVersion { + /** + * The latest, recommended platform version + */ + Latest = 'LATEST', + + /** + * Version 1.2 + * + * Supports private registries. + */ + Version12 = '1.2.0', + + /** + * Version 1.1.0 + * + * Supports task metadata, health checks, service discovery. + */ + Version11 = '1.1.0', + + /** + * Initial release + * + * Based on Amazon Linux 2017.09. + */ + Version10 = '1.0.0', +} \ No newline at end of file From 73680ef57fc660a179846d02bc92438c6628d6f2 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 1 Nov 2018 14:23:00 +0100 Subject: [PATCH 115/140] Fix integ expectation locations --- .../integ.lb-awsvpc-nw.expected.json | 0 .../integ.lb-bridge-nw.expected.json | 48 +- .../fargate/integ.lb-awsvpc-nw.expected.json | 634 ++++++++++++++++++ tools/cdk-integ-tools/lib/integ-helpers.ts | 3 +- 4 files changed, 660 insertions(+), 25 deletions(-) rename packages/@aws-cdk/aws-ecs/test/{ => ec2}/integ.lb-awsvpc-nw.expected.json (100%) rename packages/@aws-cdk/aws-ecs/test/{ => ec2}/integ.lb-bridge-nw.expected.json (91%) create mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.expected.json diff --git a/packages/@aws-cdk/aws-ecs/test/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json similarity index 100% rename from packages/@aws-cdk/aws-ecs/test/integ.lb-awsvpc-nw.expected.json rename to packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json diff --git a/packages/@aws-cdk/aws-ecs/test/integ.lb-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json similarity index 91% rename from packages/@aws-cdk/aws-ecs/test/integ.lb-bridge-nw.expected.json rename to packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json index cb0b9b13a92d2..172848a3fc864 100644 --- a/packages/@aws-cdk/aws-ecs/test/integ.lb-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json @@ -305,13 +305,13 @@ } } }, - "EcsCluster97242B84": { + "Ec2ClusterEE43E89D": { "Type": "AWS::ECS::Cluster" }, - "EcsClusterAutoScalingGroupInstanceSecurityGroupBFB09B50": { + "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37": { "Type": "AWS::EC2::SecurityGroup", "Properties": { - "GroupDescription": "aws-ecs-integ-ecs/EcsCluster/AutoScalingGroup/InstanceSecurityGroup", + "GroupDescription": "aws-ecs-integ-ecs/Ec2Cluster/AutoScalingGroup/InstanceSecurityGroup", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", @@ -323,7 +323,7 @@ "Tags": [ { "Key": "Name", - "Value": "aws-ecs-integ-ecs/EcsCluster/AutoScalingGroup" + "Value": "aws-ecs-integ-ecs/Ec2Cluster/AutoScalingGroup" } ], "VpcId": { @@ -331,7 +331,7 @@ } } }, - "EcsClusterAutoScalingGroupInstanceSecurityGroupfromawsecsintegecsLBSecurityGroup7DA90129808022F17D5A": { + "Ec2ClusterAutoScalingGroupInstanceSecurityGroupfromawsecsintegecsLBSecurityGroup7DA9012980802D795A20": { "Type": "AWS::EC2::SecurityGroupIngress", "Properties": { "IpProtocol": "tcp", @@ -339,7 +339,7 @@ "FromPort": 8080, "GroupId": { "Fn::GetAtt": [ - "EcsClusterAutoScalingGroupInstanceSecurityGroupBFB09B50", + "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37", "GroupId" ] }, @@ -352,7 +352,7 @@ "ToPort": 8080 } }, - "EcsClusterAutoScalingGroupInstanceRole2505B71D": { + "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -369,7 +369,7 @@ } } }, - "EcsClusterAutoScalingGroupInstanceRoleDefaultPolicy0DEB9F1F": { + "Ec2ClusterAutoScalingGroupInstanceRoleDefaultPolicyD987CC12": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -396,36 +396,36 @@ ], "Version": "2012-10-17" }, - "PolicyName": "EcsClusterAutoScalingGroupInstanceRoleDefaultPolicy0DEB9F1F", + "PolicyName": "Ec2ClusterAutoScalingGroupInstanceRoleDefaultPolicyD987CC12", "Roles": [ { - "Ref": "EcsClusterAutoScalingGroupInstanceRole2505B71D" + "Ref": "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C" } ] } }, - "EcsClusterAutoScalingGroupInstanceProfile77D897B8": { + "Ec2ClusterAutoScalingGroupInstanceProfile18B9D870": { "Type": "AWS::IAM::InstanceProfile", "Properties": { "Roles": [ { - "Ref": "EcsClusterAutoScalingGroupInstanceRole2505B71D" + "Ref": "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C" } ] } }, - "EcsClusterAutoScalingGroupLaunchConfig965E00BD": { + "Ec2ClusterAutoScalingGroupLaunchConfig570E562A": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { "ImageId": "ami-1234", "InstanceType": "t2.micro", "IamInstanceProfile": { - "Ref": "EcsClusterAutoScalingGroupInstanceProfile77D897B8" + "Ref": "Ec2ClusterAutoScalingGroupInstanceProfile18B9D870" }, "SecurityGroups": [ { "Fn::GetAtt": [ - "EcsClusterAutoScalingGroupInstanceSecurityGroupBFB09B50", + "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37", "GroupId" ] } @@ -437,7 +437,7 @@ [ "#!/bin/bash\necho ECS_CLUSTER=", { - "Ref": "EcsCluster97242B84" + "Ref": "Ec2ClusterEE43E89D" }, " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save" ] @@ -446,24 +446,24 @@ } }, "DependsOn": [ - "EcsClusterAutoScalingGroupInstanceRole2505B71D", - "EcsClusterAutoScalingGroupInstanceRoleDefaultPolicy0DEB9F1F" + "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C", + "Ec2ClusterAutoScalingGroupInstanceRoleDefaultPolicyD987CC12" ] }, - "EcsClusterAutoScalingGroupASGB16C0B67": { + "Ec2ClusterAutoScalingGroupASGC290B9EA": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { "MaxSize": "1", "MinSize": "0", "DesiredCapacity": "1", "LaunchConfigurationName": { - "Ref": "EcsClusterAutoScalingGroupLaunchConfig965E00BD" + "Ref": "Ec2ClusterAutoScalingGroupLaunchConfig570E562A" }, "Tags": [ { "Key": "Name", "PropagateAtLaunch": true, - "Value": "aws-ecs-integ-ecs/EcsCluster/AutoScalingGroup" + "Value": "aws-ecs-integ-ecs/Ec2Cluster/AutoScalingGroup" } ], "VPCZoneIdentifier": [ @@ -553,7 +553,7 @@ "Ref": "TaskDef54694570" }, "Cluster": { - "Ref": "EcsCluster97242B84" + "Ref": "Ec2ClusterEE43E89D" }, "DeploymentConfiguration": { "MaximumPercent": 200, @@ -621,7 +621,7 @@ } } }, - "LBSecurityGrouptoawsecsintegecsEcsClusterAutoScalingGroupInstanceSecurityGroup019ACCA48080512AF7A6": { + "LBSecurityGrouptoawsecsintegecsEc2ClusterAutoScalingGroupInstanceSecurityGroup4939BA238080198BB235": { "Type": "AWS::EC2::SecurityGroupEgress", "Properties": { "GroupId": { @@ -634,7 +634,7 @@ "Description": "Load balancer to target", "DestinationSecurityGroupId": { "Fn::GetAtt": [ - "EcsClusterAutoScalingGroupInstanceSecurityGroupBFB09B50", + "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37", "GroupId" ] }, diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.expected.json new file mode 100644 index 0000000000000..d4f7ba85f8c29 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.expected.json @@ -0,0 +1,634 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "FargateCluster7CCD5F93": { + "Type": "AWS::ECS::Cluster" + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "Links": [], + "LinuxParameters": { + "Capabilities": { + "Add": [], + "Drop": [] + }, + "Devices": [], + "Tmpfs": [] + }, + "MountPoints": [], + "Name": "web", + "PortMappings": [ + { + "ContainerPort": 80, + "Protocol": "tcp" + } + ], + "Ulimits": [], + "VolumesFrom": [] + } + ], + "Cpu": "512", + "Family": "awsecsintegTaskDef6FDFB69A", + "Memory": "1GB", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + }, + "Volumes": [] + } + }, + "ServiceD69D759B": { + "Type": "AWS::ECS::Service", + "Properties": { + "TaskDefinition": { + "Ref": "TaskDef54694570" + }, + "Cluster": { + "Ref": "FargateCluster7CCD5F93" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DesiredCount": 1, + "LaunchType": "FARGATE", + "LoadBalancers": [ + { + "ContainerName": "web", + "ContainerPort": 80, + "TargetGroupArn": { + "Ref": "LBPublicListenerFargateGroup5EE2FBAF" + } + } + ], + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "ServiceSecurityGroupC96ED6A7", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + } + } + }, + "DependsOn": [ + "LBPublicListener6E1F3D94" + ] + }, + "ServiceSecurityGroupC96ED6A7": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ/Service/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "ServiceSecurityGroupfromawsecsintegLBSecurityGroupC30F5EB480CD1B9463": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "FromPort": 80, + "GroupId": { + "Fn::GetAtt": [ + "ServiceSecurityGroupC96ED6A7", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + }, + "ToPort": 80 + } + }, + "ServiceTaskCountTarget23E25614": { + "Type": "AWS::ApplicationAutoScaling::ScalableTarget", + "Properties": { + "MaxCapacity": 10, + "MinCapacity": 1, + "ResourceId": { + "Fn::Join": [ + "", + [ + "service/", + { + "Ref": "FargateCluster7CCD5F93" + }, + "/", + { + "Fn::GetAtt": [ + "ServiceD69D759B", + "Name" + ] + } + ] + ] + }, + "RoleARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService" + ] + ] + }, + "ScalableDimension": "ecs:service:DesiredCount", + "ServiceNamespace": "ecs", + "ScheduledActions": [] + } + }, + "ServiceTaskCountTargetReasonableCpu4174EFCE": { + "Type": "AWS::ApplicationAutoScaling::ScalingPolicy", + "Properties": { + "PolicyName": "awsecsintegServiceTaskCountTargetReasonableCpuDB6AEA73", + "PolicyType": "TargetTrackingScaling", + "ScalingTargetId": { + "Ref": "ServiceTaskCountTarget23E25614" + }, + "TargetTrackingScalingPolicyConfiguration": { + "PredefinedMetricSpecification": { + "PredefinedMetricType": "ECSServiceAverageCPUUtilization" + }, + "TargetValue": 10 + } + } + }, + "LB8A12904C": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "LoadBalancerAttributes": [], + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + ], + "Type": "application" + } + }, + "LBSecurityGroup8A41EA2B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB awsecsintegLBC73915FE", + "SecurityGroupEgress": [], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow from anyone on port 80", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "LBSecurityGrouptoawsecsintegServiceSecurityGroup48EE4368807B287D7F": { + "Type": "AWS::EC2::SecurityGroupEgress", + "Properties": { + "GroupId": { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + }, + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "ServiceSecurityGroupC96ED6A7", + "GroupId" + ] + }, + "FromPort": 80, + "ToPort": 80 + } + }, + "LBPublicListener6E1F3D94": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "LBPublicListenerFargateGroup5EE2FBAF" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "LB8A12904C" + }, + "Port": 80, + "Protocol": "HTTP", + "Certificates": [] + } + }, + "LBPublicListenerFargateGroup5EE2FBAF": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "TargetGroupAttributes": [], + "Targets": [], + "TargetType": "ip" + } + } + }, + "Outputs": { + "LoadBalancerDNS": { + "Value": { + "Fn::GetAtt": [ + "LB8A12904C", + "DNSName" + ] + }, + "Export": { + "Name": "aws-ecs-integ:LoadBalancerDNS" + } + } + } +} \ No newline at end of file diff --git a/tools/cdk-integ-tools/lib/integ-helpers.ts b/tools/cdk-integ-tools/lib/integ-helpers.ts index 4cbab62415bce..ace72a8ded64a 100644 --- a/tools/cdk-integ-tools/lib/integ-helpers.ts +++ b/tools/cdk-integ-tools/lib/integ-helpers.ts @@ -56,7 +56,8 @@ export class IntegrationTest { private readonly cdkConfigPath: string; constructor(private readonly directory: string, public readonly name: string) { - this.expectedFileName = path.basename(this.name, '.js') + '.expected.json'; + const baseName = this.name.endsWith('.js') ? this.name.substr(0, this.name.length - 3) : this.name; + this.expectedFileName = baseName + '.expected.json'; this.expectedFilePath = path.join(this.directory, this.expectedFileName); this.cdkConfigPath = path.join(this.directory, 'cdk.json'); } From 61c0edb6830a68f168d9a12d090accdd8921a759 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 1 Nov 2018 14:26:47 +0100 Subject: [PATCH 116/140] Update unit test in line with new 'Dockerfile' presence test --- packages/@aws-cdk/aws-ecs/test/test.asset-image.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts index 67e716f1bb297..53428d3bfe9b4 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts @@ -1,6 +1,7 @@ import { expect, MatchStyle } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; +import path = require('path'); import ecs = require('../lib'); export = { @@ -10,7 +11,7 @@ export = { // WHEN new ecs.AssetImage(stack, 'Image', { - directory: '.' + directory: path.join(__dirname, 'demo-image'), }); // THEN From 3a7778b232d88b071ae6eafc50ea44491c39d2b2 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 1 Nov 2018 14:50:04 +0100 Subject: [PATCH 117/140] Fix test for asset image --- .../aws-ecs/lib/adopt-repository/handler.js | 4 +- packages/@aws-cdk/aws-ecs/lib/asset-image.ts | 2 +- .../fargate/integ.asset-image.expected.json | 228 ++++++++++++++++-- 3 files changed, 209 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js b/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js index d96d3614b25bb..81c3b5cdb6f02 100644 --- a/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js +++ b/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js @@ -17,7 +17,7 @@ exports.handler = async function adoptRegistryHandler(event, context) { } // The repository must already exist - async function getAdopter(name): Promise { + async function getAdopter(name) { try { const policyResponse = await ecr.getRepositoryPolicy({ repositoryName: name }).promise(); const policy = JSON.parse(policyResponse.policyText); @@ -87,7 +87,7 @@ exports.handler = async function adoptRegistryHandler(event, context) { headers: { "content-type": "", "content-length": responseBody.length } }; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { try { const request = require('https').request(requestOptions, resolve); request.on("error", reject); diff --git a/packages/@aws-cdk/aws-ecs/lib/asset-image.ts b/packages/@aws-cdk/aws-ecs/lib/asset-image.ts index d1f1fd22e739a..1e773e4000e36 100644 --- a/packages/@aws-cdk/aws-ecs/lib/asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/lib/asset-image.ts @@ -74,7 +74,7 @@ export class AssetImage extends cdk.Construct implements IContainerImage { // // If adoption fails (because the repository might be twice-adopted), we // haven't already started using the image. - const adopted = new AdoptRepository(this, 'doptRegistry', { repositoryArn: this.repositoryArn }); + const adopted = new AdoptRepository(this, 'AdoptRepository', { repositoryArn: this.repositoryArn }); this.imageName = `${adopted.repositoryUri}:${tagParameter.value}`; } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json index 34fdfd688c81a..2060fe42c0d84 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json @@ -308,20 +308,150 @@ "ClusterEB0386A7": { "Type": "AWS::ECS::Cluster" }, - "FargateServiceTaskDefTaskRole8CDCF85E": { + "ImagedoptRegistry65D733CD": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9", + "Arn" + ] + }, + "RepositoryArn": { + "Ref": "ImageRepositoryC2BE7AD4" + } + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { - "Version": "2012-10-17", "Statement": [ { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:GetRepositoryPolicy", + "ecr:SetRepositoryPolicy", + "ecr:DeleteRepository", + "ecr:ListImages", + "ecr:BatchDeleteImage" + ], "Effect": "Allow", + "Resource": { + "Ref": "ImageRepositoryC2BE7AD4" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "Roles": [ + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + } + ] + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "handler.handler", + "Role": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17", + "Arn" + ] + }, + "Runtime": "nodejs8.10", + "Timeout": 300 + }, + "DependsOn": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17", + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C" + ] + }, + "FargateServiceTaskDefTaskRole8CDCF85E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { "Action": "sts:AssumeRole", + "Effect": "Allow", "Principal": { "Service": "ecs-tasks.amazonaws.com" } } - ] + ], + "Version": "2012-10-17" } } }, @@ -336,7 +466,10 @@ "", [ { - "Ref": "ImageRepositoryC2BE7AD4" + "Fn::GetAtt": [ + "ImagedoptRegistry65D733CD", + "RepositoryUri" + ] }, ":", { @@ -384,36 +517,59 @@ "FargateServiceTaskDefTaskRole8CDCF85E", "Arn" ] - } + }, + "Volumes": [] } }, "FargateServiceTaskDefExecutionRole9194820E": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { - "Version": "2012-10-17", "Statement": [ { - "Effect": "Allow", "Action": "sts:AssumeRole", + "Effect": "Allow", "Principal": { "Service": "ecs-tasks.amazonaws.com" } } - ] + ], + "Version": "2012-10-17" + } + } + }, + "FargateServiceTaskDefExecutionRoleDefaultPolicy827E7CA2": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Ref": "ImageRepositoryC2BE7AD4" + } + }, + { + "Action": [ + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" }, - "ManagedPolicyArns": [ + "PolicyName": "FargateServiceTaskDefExecutionRoleDefaultPolicy827E7CA2", + "Roles": [ { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" - ] - ] + "Ref": "FargateServiceTaskDefExecutionRole9194820E" } ] } @@ -427,7 +583,10 @@ "Cluster": { "Ref": "ClusterEB0386A7" }, - "DeploymentConfiguration": {}, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, "DesiredCount": 1, "LaunchType": "FARGATE", "LoadBalancers": [ @@ -469,7 +628,13 @@ "Type": "AWS::EC2::SecurityGroup", "Properties": { "GroupDescription": "aws-ecs-integ/FargateService/Service/SecurityGroup", - "SecurityGroupEgress": [], + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], "SecurityGroupIngress": [], "VpcId": { "Ref": "Vpc8378EB38" @@ -597,14 +762,33 @@ "Parameters": { "ImageRepositoryC2BE7AD4": { "Type": "String", - "Description": "Repository for asset \"aws-ecs-integ/Image\"" + "Description": "Repository ARN for asset \"aws-ecs-integ/Image\"" }, "ImageTagE17D8A6B": { "Type": "String", "Description": "Tag for asset \"aws-ecs-integ/Image\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6": { + "Type": "String", + "Description": "S3 bucket for asset \"aws-ecs-integ/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": { + "Type": "String", + "Description": "S3 key for asset version \"aws-ecs-integ/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" } }, "Outputs": { + "FargateServiceLoadBalancerDNS9433D5F6": { + "Value": { + "Fn::GetAtt": [ + "FargateServiceLBB353E155", + "DNSName" + ] + }, + "Export": { + "Name": "aws-ecs-integ:FargateServiceLoadBalancerDNS9433D5F6" + } + }, "LoadBalancerDNS": { "Value": { "Fn::GetAtt": [ From ca9fcb3010f2e3faa265bdd1c31e816a7491912e Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 1 Nov 2018 16:57:09 +0100 Subject: [PATCH 118/140] Exercise handler in unit tests --- .../aws-ecs/lib/adopt-repository/handler.js | 5 +- .../@aws-cdk/aws-ecs/test/test.asset-image.ts | 54 ++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js b/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js index 81c3b5cdb6f02..2a9166fc54681 100644 --- a/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js +++ b/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js @@ -1,7 +1,8 @@ const AWS = require('aws-sdk'); const ecr = new AWS.ECR(); -exports.handler = async function adoptRegistryHandler(event, context) { +exports.handler = async function(event, context, respond) { + respond = respond || respondCFN; try { console.log(JSON.stringify(event)); @@ -65,7 +66,7 @@ exports.handler = async function adoptRegistryHandler(event, context) { await respond("FAILED", e.message, context.logStreamName, {}); } - function respond(responseStatus, reason, physId, data) { + function respondCFN(responseStatus, reason, physId, data) { const responseBody = JSON.stringify({ Status: responseStatus, Reason: reason, diff --git a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts index 53428d3bfe9b4..4540327bf5998 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts @@ -27,5 +27,57 @@ export = { }, MatchStyle.SUPERSET); test.done(); - } + }, + + async 'exercise handler'(test: Test) { + // Hijack the require('aws-sdk') statement + const Module = require('module'); + const oldRequire = Module.prototype.require; + + Module.prototype.require = (file: string) => { + if (file === 'aws-sdk') { + return { ECR }; + } + return oldRequire(file); + }; + + const handler = require(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler')); + + let output; + async function response(responseStatus: string, reason: string, physId: string, data: any) { + output = { responseStatus, reason, physId, data }; + } + + await handler.handler({ + StackId: 'StackId', + ResourceProperties: { + RepositoryArn: 'RepositoryArn', + }, + RequestType: 'Create', + ResponseURL: 'https://localhost/test' + }, { + logStreamName: 'xyz', + }, response); + + test.deepEqual(output, { + responseStatus: 'SUCCESS', + reason: 'OK', + physId: '', + data: { RepositoryUri: 'undefined.dkr.ecr.undefined.amazonaws.com/' } + }); + + test.done(); + }, }; + +class ECR { + public getRepositoryPolicy() { + return { async promise() { return { + policyText: '{"asdf": "asdf"}' + }; } }; + } + + public setRepositoryPolicy() { + return { async promise() { return; } }; + } +} \ No newline at end of file From 0dbbe63cc5f9f23970cd90f3c7a6230bfeed4d26 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 1 Nov 2018 14:39:54 -0700 Subject: [PATCH 119/140] Fix container healthCheck defaults --- .../aws-ecs/lib/container-definition.ts | 7 +- .../aws-ecs/test/test.container-definition.ts | 78 ++++++++++++++++--- 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 185f4dc8f8e88..12377078e48ea 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -359,7 +359,6 @@ export class ContainerDefinition extends cdk.Construct { healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), links: this.links, linuxParameters: this.linuxParameters.renderLinuxParameters(), - }; } } @@ -423,10 +422,10 @@ function renderKV(env: {[key: string]: string}, keyName: string, valueName: stri function renderHealthCheck(hc: HealthCheck): cloudformation.TaskDefinitionResource.HealthCheckProperty { return { command: getHealthCheckCommand(hc), - interval: hc.intervalSeconds, - retries: hc.retries, + interval: hc.intervalSeconds !== undefined ? hc.intervalSeconds : 30, + retries: hc.retries !== undefined ? hc.retries : 3, startPeriod: hc.startPeriod, - timeout: hc.timeout + timeout: hc.timeout !== undefined ? hc.timeout : 5, }; } diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index a4599a7c3676b..63f7197d4edc3 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -148,15 +148,6 @@ export = { }, } - // "With health check": { - // "healthCheck.command is a single string"(test: Test) { - // const stack = new cdk.Stack(); - // const taskDefinition = new TaskDefinition(stack, 'TaskDef'); - // const containerDefinition = taskDefinition.ContainerDefinition[0]; - // test.deepEqual(resolve(vpc.vpcId), {Ref: 'TheVPC92636AB0' } ); - // test.done(); - // }, - // } }, "Ingress Port": { "With network mode AwsVpc": { @@ -292,4 +283,73 @@ export = { test.done(); }, + 'can set Health Check with defaults'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + const hcCommand = "curl localhost:8000"; + + // WHEN + taskDefinition.addContainer('cont', { + image: ecs.DockerHub.image('test'), + memoryLimitMiB: 1024, + healthCheck: { + command: [hcCommand] + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + HealthCheck: { + Command: ["CMD-SHELL", hcCommand], + Interval: 30, + Retries: 3, + Timeout: 5 + }, + } + ] + })); + + test.done(); + }, + + 'can specify Health Check values'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + const hcCommand = "curl localhost:8000"; + + // WHEN + taskDefinition.addContainer('cont', { + image: ecs.DockerHub.image('test'), + memoryLimitMiB: 1024, + healthCheck: { + command: [hcCommand], + intervalSeconds: 20, + retries: 5, + startPeriod: 10 + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + HealthCheck: { + Command: ["CMD-SHELL", hcCommand], + Interval: 20, + Retries: 5, + Timeout: 5, + StartPeriod: 10 + }, + } + ] + })); + + test.done(); + }, + + // render extra hosts test }; \ No newline at end of file From cc695198deaca68488dcf19e0e19e3c65fa36d8c Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Thu, 1 Nov 2018 21:07:36 -0700 Subject: [PATCH 120/140] Add ScratchSpace method Easier way of adding volumes and mountPoints to a Task Definition --- .../aws-ecs/lib/base/base-task-definition.ts | 2 +- .../aws-ecs/lib/container-definition.ts | 31 +++++++++++++- .../test/ec2/test.ec2-task-definition.ts | 40 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts index 61265cafd15a5..945649ed1fd9f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts @@ -146,7 +146,7 @@ export abstract class BaseTaskDefinition extends cdk.Construct { /** * Add a volume to this task definition */ - private addVolume(volume: Volume) { + public addVolume(volume: Volume) { this.volumes.push(volume); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 12377078e48ea..6c42f2e40e338 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -249,12 +249,34 @@ export class ContainerDefinition extends cdk.Construct { } /** - * Add one or more mount points to this container + * Add one or more mount points to this container. */ public addMountPoints(...mountPoints: MountPoint[]) { this.mountPoints.push(...mountPoints); } + /** + * Mount temporary disc space to a container. + * This adds the correct container mountPoint and task definition volume. + */ + public addScratch(scratch: ScratchSpace) { + const mountPoint = { + containerPath: scratch.containerPath, + readOnly: scratch.readOnly, + sourceVolume: scratch.name + }; + + const volume = { + host: { + sourcePath: scratch.sourcePath + }, + name: scratch.name + }; + + this.taskDefinition.addVolume(volume); + this.addMountPoints(mountPoint); + } + /** * Add one or more port mappings to this container */ @@ -553,6 +575,13 @@ function renderPortMapping(pm: PortMapping): cloudformation.TaskDefinitionResour }; } +export interface ScratchSpace { + containerPath: string, + readOnly: boolean, + sourcePath: string + name: string, +} + export interface MountPoint { containerPath: string, readOnly: boolean, diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts index 6cb9e9f2aab01..346d53e8e3bdd 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts @@ -98,6 +98,46 @@ export = { test.done(); }, + "correctly sets scratch space"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + const container = taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + container.addScratch({ + containerPath: "./cache", + readOnly: true, + sourcePath: "/tmp/cache", + name: "scratch" + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + Family: "Ec2TaskDef", + ContainerDefinitions: [{ + MountPoints: [ + { + ContainerPath: "./cache", + ReadOnly: true, + SourceVolume: "scratch" + } + ] + }], + Volumes: [{ + Host: { + SourcePath: "/tmp/cache" + }, + Name: "scratch" + }] + })); + + test.done(); + }, + "correctly sets volumes"(test: Test) { // GIVEN const stack = new cdk.Stack(); From 645b9a01def643adc7a0e5de374e7691c26fdf30 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 2 Nov 2018 11:37:19 +0100 Subject: [PATCH 121/140] Fix aws-sdk stubbing by bringing in an external dependency --- packages/@aws-cdk/aws-ecs/package.json | 4 +++- .../@aws-cdk/aws-ecs/test/test.asset-image.ts | 16 ++++------------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index f20305510f1b3..788eb15dd14c7 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -56,7 +56,9 @@ "cdk-build-tools": "^0.14.1", "cdk-integ-tools": "^0.14.1", "cfn2ts": "^0.14.1", - "pkglint": "^0.14.1" + "pkglint": "^0.14.1", + "proxyquire": "^2.1.0", + "@types/proxyquire": "^1.3.28" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "^0.14.1", diff --git a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts index 4540327bf5998..775b515c1aa11 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts @@ -2,6 +2,7 @@ import { expect, MatchStyle } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import path = require('path'); +import proxyquire = require('proxyquire'); import ecs = require('../lib'); export = { @@ -30,18 +31,9 @@ export = { }, async 'exercise handler'(test: Test) { - // Hijack the require('aws-sdk') statement - const Module = require('module'); - const oldRequire = Module.prototype.require; - - Module.prototype.require = (file: string) => { - if (file === 'aws-sdk') { - return { ECR }; - } - return oldRequire(file); - }; - - const handler = require(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler')); + const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler'), { + 'aws-sdk': { '@noCallThru': true, ECR } + }); let output; async function response(responseStatus: string, reason: string, physId: string, data: any) { From 4ed101b9729a166899c293c66c57280f8a2beccd Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 2 Nov 2018 15:19:14 +0100 Subject: [PATCH 122/140] Fix handler, update expectation --- .../@aws-cdk/aws-ecs/lib/adopt-repository/handler.js | 10 +++++----- .../test/fargate/integ.asset-image.expected.json | 4 ++-- packages/@aws-cdk/aws-ecs/test/test.asset-image.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js b/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js index 2a9166fc54681..640b5d4e538f3 100644 --- a/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js +++ b/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js @@ -1,7 +1,7 @@ const AWS = require('aws-sdk'); const ecr = new AWS.ECR(); -exports.handler = async function(event, context, respond) { +exports.handler = async function(event, context, _callback, respond) { respond = respond || respondCFN; try { console.log(JSON.stringify(event)); @@ -36,13 +36,13 @@ exports.handler = async function(event, context, respond) { if (adopter.Sid !== markerStatement.Sid) { throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); } - console.log('Deleting', repo); - const ids = (await ecr.listImages({ repositoryName: repo }).promise()).imageIds; try { + console.log('Deleting', repo); + const ids = (await ecr.listImages({ repositoryName: repo }).promise()).imageIds; await ecr.batchDeleteImage({ repositoryName: repo, imageIds: ids }).promise(); await ecr.deleteRepository({ repositoryName: repo }).promise(); - } catch (e) { - if (e.code !== 'RepositoryPolicyNotFoundException') { throw e; } + } catch(e) { + if (e.code !== 'RepositoryNotFoundException') { throw e; } } } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json index 2060fe42c0d84..083b5b9ba3d22 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json @@ -308,7 +308,7 @@ "ClusterEB0386A7": { "Type": "AWS::ECS::Cluster" }, - "ImagedoptRegistry65D733CD": { + "ImageAdoptRepositoryE1E84E35": { "Type": "AWS::CloudFormation::CustomResource", "Properties": { "ServiceToken": { @@ -467,7 +467,7 @@ [ { "Fn::GetAtt": [ - "ImagedoptRegistry65D733CD", + "ImageAdoptRepositoryE1E84E35", "RepositoryUri" ] }, diff --git a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts index 775b515c1aa11..ef105dbca525f 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts @@ -49,7 +49,7 @@ export = { ResponseURL: 'https://localhost/test' }, { logStreamName: 'xyz', - }, response); + }, undefined, response); test.deepEqual(output, { responseStatus: 'SUCCESS', From 54e4665500fc8114228c84fcc6ddfb6577170977 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 2 Nov 2018 16:18:50 +0100 Subject: [PATCH 123/140] Minor tweaks that Clare wanted to see --- packages/@aws-cdk/aws-ecs/README.md | 2 ++ packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 1f30516772059..819f063976607 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -161,3 +161,5 @@ EC2 instance group so that your instance count scales with demand. - [ ] Instance AutoScaling - [ ] Service Discovery Integration +- [ ] Private registry authentication + diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 64e10856bafde..96ee71e243beb 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -102,19 +102,19 @@ export enum FargatePlatformVersion { * * Supports private registries. */ - Version12 = '1.2.0', + Version1_2 = '1.2.0', /** * Version 1.1.0 * * Supports task metadata, health checks, service discovery. */ - Version11 = '1.1.0', + Version1_1 = '1.1.0', /** * Initial release * * Based on Amazon Linux 2017.09. */ - Version10 = '1.0.0', + Version1_0 = '1.0.0', } \ No newline at end of file From e2d338f8bb3b5e896b906a5d51e14b011024b826 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 2 Nov 2018 16:22:53 +0100 Subject: [PATCH 124/140] Rename autoscaling methods so C# doesn't complain --- .../lib/base-scalable-attribute.ts | 6 +++--- .../aws-dynamodb/lib/scalable-table-attribute.ts | 4 ++-- .../aws-ecs/lib/base/scalable-task-count.ts | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scalable-attribute.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scalable-attribute.ts index 4f7ebc66e55b2..94aa6e8764cd2 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scalable-attribute.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scalable-attribute.ts @@ -62,21 +62,21 @@ export abstract class BaseScalableAttribute extends cdk.Construct { /** * Scale out or in based on time */ - protected scaleOnSchedule(id: string, props: ScalingSchedule) { + protected doScaleOnSchedule(id: string, props: ScalingSchedule) { this.target.scaleOnSchedule(id, props); } /** * Scale out or in based on a metric value */ - protected scaleOnMetric(id: string, props: BasicStepScalingPolicyProps) { + protected doScaleOnMetric(id: string, props: BasicStepScalingPolicyProps) { this.target.scaleOnMetric(id, props); } /** * Scale out or in in order to keep a metric around a target value */ - protected scaleToTrackMetric(id: string, props: BasicTargetTrackingScalingPolicyProps) { + protected doScaleToTrackMetric(id: string, props: BasicTargetTrackingScalingPolicyProps) { this.target.scaleToTrackMetric(id, props); } } diff --git a/packages/@aws-cdk/aws-dynamodb/lib/scalable-table-attribute.ts b/packages/@aws-cdk/aws-dynamodb/lib/scalable-table-attribute.ts index 7a7c1f4dcbbcd..a0fb5e83bf946 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/scalable-table-attribute.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/scalable-table-attribute.ts @@ -9,7 +9,7 @@ export class ScalableTableAttribute extends appscaling.BaseScalableAttribute { * Scale out or in based on time */ public scaleOnSchedule(id: string, action: appscaling.ScalingSchedule) { - super.scaleOnSchedule(id, action); + super.doScaleOnSchedule(id, action); } /** @@ -24,7 +24,7 @@ export class ScalableTableAttribute extends appscaling.BaseScalableAttribute { ? appscaling.PredefinedMetric.DynamoDBWriteCapacityUtilization : appscaling.PredefinedMetric.DynamoDBReadCapacityUtilization; - super.scaleToTrackMetric('Tracking', { + super.doScaleToTrackMetric('Tracking', { policyName: props.policyName, disableScaleIn: props.disableScaleIn, scaleInCooldownSec: props.scaleInCooldownSec, diff --git a/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts b/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts index f63e42a18d1a3..564058072d712 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts @@ -8,22 +8,22 @@ export class ScalableTaskCount extends appscaling.BaseScalableAttribute { /** * Scale out or in based on time */ - public scaleOnSchedule(id: string, props: appscaling.ScalingSchedule) { - return super.scaleOnSchedule(id, props); + public doScaleOnSchedule(id: string, props: appscaling.ScalingSchedule) { + return super.doScaleOnSchedule(id, props); } /** * Scale out or in based on a metric value */ - public scaleOnMetric(id: string, props: appscaling.BasicStepScalingPolicyProps) { - return super.scaleOnMetric(id, props); + public doScaleOnMetric(id: string, props: appscaling.BasicStepScalingPolicyProps) { + return super.doScaleOnMetric(id, props); } /** * Scale out or in to achieve a target CPU utilization */ public scaleOnCpuUtilization(id: string, props: CpuUtilizationScalingProps) { - return super.scaleToTrackMetric(id, { + return super.doScaleToTrackMetric(id, { predefinedMetric: appscaling.PredefinedMetric.ECSServiceAverageCPUUtilization, policyName: props.policyName, disableScaleIn: props.disableScaleIn, @@ -37,7 +37,7 @@ export class ScalableTaskCount extends appscaling.BaseScalableAttribute { * Scale out or in to achieve a target memory utilization utilization */ public scaleOnMemoryUtilization(id: string, props: CpuUtilizationScalingProps) { - return super.scaleToTrackMetric(id, { + return super.doScaleToTrackMetric(id, { predefinedMetric: appscaling.PredefinedMetric.ECSServiceAverageMemoryUtilization, targetValue: props.targetUtilizationPercent, policyName: props.policyName, @@ -51,7 +51,7 @@ export class ScalableTaskCount extends appscaling.BaseScalableAttribute { * Scale out or in to track a custom metric */ public scaleToTrackCustomMetric(id: string, props: TrackCustomMetricProps) { - return super.scaleToTrackMetric(id, { + return super.doScaleToTrackMetric(id, { customMetric: props.metric, targetValue: props.targetValue, policyName: props.policyName, From b0f54a8afc92a9ea662e1feefa7e02bd0a044807 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 2 Nov 2018 16:41:23 +0100 Subject: [PATCH 125/140] Set ECS_AWSVPC_BLOCK_IMDS=true config option --- packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts index 3b0ef66124508..3a55644d70c60 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts @@ -73,6 +73,8 @@ export class Ec2Cluster extends BaseCluster implements IEc2Cluster { // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html autoScalingGroup.addUserData('sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP'); autoScalingGroup.addUserData('sudo service iptables save'); + // The following is only for AwsVpc networking mode, but doesn't hurt for the other modes. + autoScalingGroup.addUserData('echo ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config'); } // ECS instances must be able to do these things From 8f0f6b14335a0bdeed7d51fc8dd3a5f5c3288138 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Sun, 4 Nov 2018 17:25:52 +0100 Subject: [PATCH 126/140] Update test expectation --- .../aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json | 4 ++-- .../aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json | 4 ++-- packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json index 233d2d162f2d1..7fe6647326286 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json @@ -418,7 +418,7 @@ { "Ref": "Ec2ClusterEE43E89D" }, - " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save" + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" ] ] } @@ -725,4 +725,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json index 172848a3fc864..bf22690292b47 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json @@ -439,7 +439,7 @@ { "Ref": "Ec2ClusterEE43E89D" }, - " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save" + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" ] ] } @@ -688,4 +688,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts index 2f5c6a4da8415..126a030bbd80d 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts @@ -54,7 +54,7 @@ export = { Ref: "Ec2ClusterEE43E89D" }, // tslint:disable-next-line:max-line-length - " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save" + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" ] ] } From 15b8804b85cdde12de1863d20aa7580bb96698c2 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Sun, 4 Nov 2018 17:26:21 +0100 Subject: [PATCH 127/140] Add helper scripts to compile sources faster --- scripts/build-typescript.sh | 42 +++++++++++++++++++ scripts/dependencies.py | 82 +++++++++++++++++++++++++++++++++++++ scripts/regen-l1.sh | 6 +++ 3 files changed, 130 insertions(+) create mode 100755 scripts/build-typescript.sh create mode 100644 scripts/dependencies.py create mode 100755 scripts/regen-l1.sh diff --git a/scripts/build-typescript.sh b/scripts/build-typescript.sh new file mode 100755 index 0000000000000..965a3fe38d5df --- /dev/null +++ b/scripts/build-typescript.sh @@ -0,0 +1,42 @@ +#!/bin/bash +cat < tsconfig.json +{ + "compilerOptions": { + "alwaysStrict": true, + "charset": "utf8", + "declaration": false, + "experimentalDecorators": true, + "inlineSourceMap": true, + "inlineSources": true, + "lib": [ + "es2016", + "es2017.object", + "es2017.string" + ], + "module": "CommonJS", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "target": "ES2018" + }, + "include": [ + "packages/**/*.ts", + "tools/**/*.ts" + ], + "exclude": [ + "node_modules", + "packages/@aws-cdk/aws-sns/examples", + "tools/cfn2ts/test/enrichments", + "packages/aws-cdk/lib/init-templates" + ], + "_generated_by_jsii_": "Generated by jsii - safe to delete, and ideally should be in .gitignore" +} +EOF +tsc -p . "$@" diff --git a/scripts/dependencies.py b/scripts/dependencies.py new file mode 100644 index 0000000000000..459c8829c2170 --- /dev/null +++ b/scripts/dependencies.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +import json +import sys +import collections +import os +from os import path + + +def full_dependency_graph(): + """Return a map of { package -> [package] }.""" + graph = collections.defaultdict(set) + for filename in package_jsons(): + with open(filename) as f: + package_json = json.load(f) + + for key in ['devDependencies', 'dependencies']: + if key in package_json: + graph[package_json['name']].update(package_json[key].keys()) + + return graph + + +def local_dependency_graph(): + """Retain only the dependencies that are also in the repo.""" + graph = full_dependency_graph() + for deps in graph.values(): + deps.intersection_update(graph.keys()) + return graph + + +def package_jsons(): + """Return a list of all package.json files in this project.""" + rootdir = path.dirname(path.dirname(path.realpath(__file__))) + for root, dirs, files in os.walk(rootdir): + if 'node_modules' in dirs: + dirs.remove('node_modules') + + if 'package.json' in files: + yield path.join(root, 'package.json') + + +def find(xs, x): + for i, value in enumerate(xs): + if x == value: + return i + return None + + +def print_graph(graph): + for package, deps in graph.items(): + for dep in deps: + print('%s -> %s' % (package, dep)) + + checked = set() + + # Do a check for cycles for each package. This is slow but it works, + # and it has the advantage that it can give good diagnostics. + def check_for_cycles(package, path): + i = find(path, package) + if i is not None: + cycle = path[i:] + [package] + print('Cycle: %s' % ' => '.join(cycle)) + return + + if package in checked: + return + + checked.add(package) + + deps = graph.get(package, []) + for dep in deps: + check_for_cycles(dep, path + [package]) + + for package in graph.keys(): + check_for_cycles(package, []) + +def main(): + print_graph(local_dependency_graph()) + + +if __name__ == '__main__': + main() diff --git a/scripts/regen-l1.sh b/scripts/regen-l1.sh new file mode 100755 index 0000000000000..bc6dd410b9e7b --- /dev/null +++ b/scripts/regen-l1.sh @@ -0,0 +1,6 @@ +#!/bin/bash +rm -f packages/@aws-cdk/*/lib/*.generated.* +node_modules/.bin/lerna --scope @aws-cdk/cfnspec run build +node_modules/.bin/lerna --scope cfn2ts run build +cfn2ts=$(pwd)/tools/cfn2ts/bin/cfn2ts +node_modules/.bin/lerna --concurrency=1 --no-bail exec -- bash -c "pwd && $cfn2ts --scope \$(node -p 'require(\"./package.json\")[\"cdk-build\"].cloudformation')" From 07f1f5798fb8a9728592bc5ee693c904f43fedee Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Sun, 4 Nov 2018 21:02:24 +0100 Subject: [PATCH 128/140] Extract capacity from EC2 cluster Make Connections be able to manage multiple security groups. Give broad permissions again, otherwise it doesn't work --- .../hello-cdk-ecs/index.ts | 10 +- .../aws-autoscaling/lib/auto-scaling-group.ts | 2 +- packages/@aws-cdk/aws-ec2/lib/connections.ts | 125 ++++++--- .../@aws-cdk/aws-ec2/lib/security-group.ts | 2 +- .../@aws-cdk/aws-ec2/test/test.connections.ts | 239 +++++++----------- .../aws-ec2/test/test.security-group.ts | 175 +++++++++++++ packages/@aws-cdk/aws-ecs/README.md | 38 ++- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 12 +- .../@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts | 108 ++++---- .../@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts | 10 +- .../aws-ecs/lib/fargate/fargate-service.ts | 8 +- .../test/ec2/integ.lb-awsvpc-nw.expected.json | 39 ++- .../aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts | 3 + .../test/ec2/integ.lb-bridge-nw.expected.json | 47 ++-- .../aws-ecs/test/ec2/integ.lb-bridge-nw.ts | 3 + .../aws-ecs/test/ec2/test.ec2-cluster.ts | 74 +++--- .../lib/load-balancer.ts | 4 +- .../lib/alb/application-listener.ts | 6 +- .../lib/alb/application-load-balancer.ts | 4 +- .../test/helpers.ts | 2 +- .../@aws-cdk/aws-lambda/lib/lambda-ref.ts | 9 +- packages/@aws-cdk/aws-lambda/lib/lambda.ts | 2 +- .../aws-lambda/test/test.vpc-lambda.ts | 4 +- .../@aws-cdk/aws-quickstarts/lib/database.ts | 2 +- packages/@aws-cdk/aws-quickstarts/lib/rdgw.ts | 2 +- packages/@aws-cdk/aws-rds/lib/cluster-ref.ts | 2 +- packages/@aws-cdk/aws-rds/lib/cluster.ts | 2 +- 27 files changed, 568 insertions(+), 366 deletions(-) create mode 100644 packages/@aws-cdk/aws-ec2/test/test.security-group.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index 0a103e5619dec..c9e0a46a86002 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -12,10 +12,10 @@ class BonjourECS extends cdk.Stack { // deploy, but VPC creation is slow so we'll only have to do that once // and can iterate quickly on consuming stacks. Not doing that for now. const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); - const cluster = new ecs.Ec2Cluster(this, 'Ec2Cluster', { - vpc, - size: 3, - instanceType: new InstanceType("t2.xlarge") + const cluster = new ecs.Ec2Cluster(this, 'Ec2Cluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ + instanceType: new InstanceType("t2.xlarge"), + instanceCount: 3, }); // Instantiate ECS Service with just cluster and image @@ -34,4 +34,4 @@ const app = new cdk.App(); new BonjourECS(app, 'Bonjour'); -app.run(); \ No newline at end of file +app.run(); diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 584240c470701..2a075b16a9ca3 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -183,7 +183,7 @@ export class AutoScalingGroup extends cdk.Construct implements cdk.ITaggable, el vpc: props.vpc, allowAllOutbound: props.allowAllOutbound !== false }); - this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); + this.connections = new ec2.Connections({ securityGroups: [this.securityGroup] }); this.securityGroups.push(this.securityGroup); this.tags = new TagManager(this, {initialTags: props.tags}); this.tags.setTag(NAME_TAG, this.path, { overwrite: false }); diff --git a/packages/@aws-cdk/aws-ec2/lib/connections.ts b/packages/@aws-cdk/aws-ec2/lib/connections.ts index b1bce135e4f44..cd416a4c7ee1e 100644 --- a/packages/@aws-cdk/aws-ec2/lib/connections.ts +++ b/packages/@aws-cdk/aws-ec2/lib/connections.ts @@ -36,11 +36,11 @@ export interface ConnectionsProps { securityGroupRule?: ISecurityGroupRule; /** - * What securityGroup this object is managing connections for + * What securityGroup(s) this object is managing connections for * - * @default No security + * @default No security groups */ - securityGroup?: SecurityGroupRef; + securityGroups?: SecurityGroupRef[]; /** * Default port range for initiating connections to and from this object @@ -59,68 +59,102 @@ export interface ConnectionsProps { * establishing connectivity between security groups, it will automatically * add rules in both security groups * + * This object can manage one or more security groups. */ -export class Connections { +export class Connections implements IConnectable { + public readonly connections: Connections; + + /** + * The default port configured for this connection peer, if available + */ + public readonly defaultPortRange?: IPortRange; + /** * Underlying securityGroup for this Connections object, if present * * May be empty if this Connections object is not managing a SecurityGroup, * but simply representing a Connectable peer. */ - public readonly securityGroup?: SecurityGroupRef; + private readonly _securityGroups = new ReactiveList(); /** * The rule that defines how to represent this peer in a security group */ - public readonly securityGroupRule: ISecurityGroupRule; + private readonly _securityGroupRules = new ReactiveList(); - /** - * The default port configured for this connection peer, if available - */ - public readonly defaultPortRange?: IPortRange; + private skip: boolean = false; - constructor(props: ConnectionsProps) { - if (!props.securityGroupRule && !props.securityGroup) { - throw new Error('Connections: require one of securityGroupRule or securityGroup'); + constructor(props: ConnectionsProps = {}) { + this.connections = this; + this._securityGroups.push(...(props.securityGroups || [])); + + this._securityGroupRules.push(...this._securityGroups.asArray()); + if (props.securityGroupRule) { + this._securityGroupRules.push(props.securityGroupRule); } - this.securityGroupRule = props.securityGroupRule || props.securityGroup!; - this.securityGroup = props.securityGroup; this.defaultPortRange = props.defaultPortRange; } + public get securityGroups(): SecurityGroupRef[] { + return this._securityGroups.asArray(); + } + + /** + * Add a security group to the list of security groups managed by this object + */ + public addSecurityGroup(...securityGroups: SecurityGroupRef[]) { + for (const securityGroup of securityGroups) { + this._securityGroups.push(securityGroup); + this._securityGroupRules.push(securityGroup); + } + } + /** * Allow connections to the peer on the given port */ public allowTo(other: IConnectable, portRange: IPortRange, description?: string) { - if (this.securityGroup) { - this.securityGroup.addEgressRule(other.connections.securityGroupRule, portRange, description); - } - if (other.connections.securityGroup) { - other.connections.securityGroup.addIngressRule(this.securityGroupRule, portRange, description); + if (this.skip) { return; } - } + this._securityGroups.forEachAndForever(securityGroup => { + other.connections._securityGroupRules.forEachAndForever(rule => { + securityGroup.addEgressRule(rule, portRange, description); + }); + }); + + this.skip = true; + other.connections.allowFrom(this, portRange, description); + this.skip = false; } /** * Allow connections from the peer on the given port */ public allowFrom(other: IConnectable, portRange: IPortRange, description?: string) { - if (this.securityGroup) { - this.securityGroup.addIngressRule(other.connections.securityGroupRule, portRange, description); - } - if (other.connections.securityGroup) { - other.connections.securityGroup.addEgressRule(this.securityGroupRule, portRange, description); - } + if (this.skip) { return; } + + this._securityGroups.forEachAndForever(securityGroup => { + other.connections._securityGroupRules.forEachAndForever(rule => { + securityGroup.addIngressRule(rule, portRange, description); + }); + }); + + this.skip = true; + other.connections.allowTo(this, portRange, description); + this.skip = false; } /** * Allow hosts inside the security group to connect to each other on the given port */ public allowInternally(portRange: IPortRange, description?: string) { - if (this.securityGroup) { - this.securityGroup.addIngressRule(this.securityGroupRule, portRange, description); - } + this._securityGroups.forEachAndForever(securityGroup => { + this._securityGroupRules.forEachAndForever(rule => { + securityGroup.addIngressRule(rule, portRange, description); + // FIXME: this seems required but we didn't use to have it. Research. + // securityGroup.addEgressRule(rule, portRange, description); + }); + }); } /** @@ -192,3 +226,34 @@ export class Connections { this.allowTo(other, this.defaultPortRange, description); } } + +type Action = (x: T) => void; + +class ReactiveList { + private readonly elements = new Array(); + private readonly listeners = new Array>(); + + public push(...xs: T[]) { + this.elements.push(...xs); + for (const listener of this.listeners) { + for (const x of xs) { + listener(x); + } + } + } + + public forEachAndForever(listener: Action) { + for (const element of this.elements) { + listener(element); + } + this.listeners.push(listener); + } + + public asArray(): T[] { + return this.elements.slice(); + } + + public get length(): number { + return this.elements.length; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index 49b187bc107bd..c2d64f1eaa544 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -24,7 +24,7 @@ export abstract class SecurityGroupRef extends Construct implements ISecurityGro public abstract readonly securityGroupId: string; public readonly canInlineRule = false; - public readonly connections = new Connections({ securityGroup: this }); + public readonly connections = new Connections({ securityGroups: [this] }); /** * FIXME: Where to place this?? diff --git a/packages/@aws-cdk/aws-ec2/test/test.connections.ts b/packages/@aws-cdk/aws-ec2/test/test.connections.ts index 6a56884152f91..eea6a340b7a50 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.connections.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.connections.ts @@ -3,231 +3,162 @@ import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { - AllTraffic, - AnyIPv4, - AnyIPv6, Connections, - IcmpAllTypeCodes, - IcmpAllTypesAndCodes, - IcmpPing, - IcmpTypeAndCode, IConnectable, - PrefixList, SecurityGroup, SecurityGroupRef, TcpAllPorts, TcpPort, - TcpPortFromAttribute, - TcpPortRange, - UdpAllPorts, - UdpPort, - UdpPortFromAttribute, - UdpPortRange, VpcNetwork } from "../lib"; export = { - 'security group can allows all outbound traffic by default'(test: Test) { + 'peering between two security groups does not recursive infinitely'(test: Test) { // GIVEN - const stack = new Stack(); + 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 }); - // WHEN - new SecurityGroup(stack, 'SG1', { vpc, allowAllOutbound: true }); + const conn1 = new SomethingConnectable(new Connections({ securityGroups: [sg1] })); + const conn2 = new SomethingConnectable(new Connections({ securityGroups: [sg2] })); - // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - SecurityGroupEgress: [ - { - CidrIp: "0.0.0.0/0", - Description: "Allow all outbound traffic by default", - IpProtocol: "-1" - } - ], - })); + // WHEN + conn1.connections.allowTo(conn2, new TcpPort(80), 'Test'); + // THEN -- it finishes! test.done(); }, - 'no new outbound rule is added if we are allowing all traffic anyway'(test: Test) { + '(imported) SecurityGroup can be used as target of .allowTo()'(test: Test) { // GIVEN const stack = new Stack(); const vpc = new VpcNetwork(stack, 'VPC'); + const sg1 = new SecurityGroup(stack, 'SomeSecurityGroup', { vpc, allowAllOutbound: false }); + const somethingConnectable = new SomethingConnectable(new Connections({ securityGroups: [sg1] })); + + const securityGroup = SecurityGroupRef.import(stack, 'ImportedSG', { securityGroupId: 'sg-12345' }); // WHEN - const sg = new SecurityGroup(stack, 'SG1', { vpc, allowAllOutbound: true }); - sg.addEgressRule(new AnyIPv4(), new TcpPort(86), 'This does not show up'); + somethingConnectable.connections.allowTo(securityGroup, new TcpAllPorts(), 'Connect there'); - // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - SecurityGroupEgress: [ - { - CidrIp: "0.0.0.0/0", - Description: "Allow all outbound traffic by default", - IpProtocol: "-1" - }, - ], + // THEN: rule to generated security group to connect to imported + expect(stack).to(haveResource("AWS::EC2::SecurityGroupEgress", { + GroupId: { "Fn::GetAtt": [ "SomeSecurityGroupEF219AD6", "GroupId" ] }, + IpProtocol: "tcp", + Description: "Connect there", + DestinationSecurityGroupId: "sg-12345", + FromPort: 0, + ToPort: 65535 + })); + + // THEN: rule to imported security group to allow connections from generated + expect(stack).to(haveResource("AWS::EC2::SecurityGroupIngress", { + IpProtocol: "tcp", + Description: "Connect there", + FromPort: 0, + GroupId: "sg-12345", + SourceSecurityGroupId: { "Fn::GetAtt": [ "SomeSecurityGroupEF219AD6", "GroupId" ] }, + ToPort: 65535 })); test.done(); }, - 'security group disallow outbound traffic by default'(test: Test) { + 'security groups added to connections after rule still gets rule'(test: Test) { // GIVEN const stack = new Stack(); const vpc = new VpcNetwork(stack, 'VPC'); + const sg1 = new SecurityGroup(stack, 'SecurityGroup1', { vpc, allowAllOutbound: false }); + const sg2 = new SecurityGroup(stack, 'SecurityGroup2', { vpc, allowAllOutbound: false }); + const connections = new Connections({ securityGroups: [sg1] }); // WHEN - new SecurityGroup(stack, 'SG1', { vpc, allowAllOutbound: false }); + connections.allowFromAnyIPv4(new TcpPort(88)); + connections.addSecurityGroup(sg2); // THEN expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - SecurityGroupEgress: [ + GroupDescription: "SecurityGroup1", + SecurityGroupIngress: [ { - CidrIp: "255.255.255.255/32", - Description: "Disallow all traffic", - FromPort: 252, - IpProtocol: "icmp", - ToPort: 86 + CidrIp: "0.0.0.0/0", + FromPort: 88, + ToPort: 88 } - ], + ] })); - test.done(); - }, - - 'bogus outbound rule disappears if another rule is added'(test: Test) { - // GIVEN - const stack = new Stack(); - const vpc = new VpcNetwork(stack, 'VPC'); - - // WHEN - const sg = new SecurityGroup(stack, 'SG1', { vpc, allowAllOutbound: false }); - sg.addEgressRule(new AnyIPv4(), new TcpPort(86), 'This replaces the other one'); - - // THEN expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - SecurityGroupEgress: [ + GroupDescription: "SecurityGroup2", + SecurityGroupIngress: [ { CidrIp: "0.0.0.0/0", - Description: "This replaces the other one", - FromPort: 86, - IpProtocol: "tcp", - ToPort: 86 + FromPort: 88, + ToPort: 88 } - ], + ] })); test.done(); }, - 'all outbound rule cannot be added after creation'(test: Test) { + 'when security groups are added to target they also get the rule'(test: Test) { // GIVEN const stack = new Stack(); const vpc = new VpcNetwork(stack, 'VPC'); + const sg1 = new SecurityGroup(stack, 'SecurityGroup1', { vpc, allowAllOutbound: false }); + const sg2 = new SecurityGroup(stack, 'SecurityGroup2', { vpc, allowAllOutbound: false }); + const sg3 = new SecurityGroup(stack, 'SecurityGroup3', { vpc, allowAllOutbound: false }); + const connections1 = new Connections({ securityGroups: [sg1] }); + const connections2 = new Connections({ securityGroups: [sg2] }); + const connectable = new SomethingConnectable(connections2); // WHEN - const sg = new SecurityGroup(stack, 'SG1', { vpc, allowAllOutbound: false }); - test.throws(() => { - sg.addEgressRule(new AnyIPv4(), new AllTraffic(), 'All traffic'); - }, /Cannot add/); - - test.done(); - }, - - '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 SomethingConnectable(new Connections({ securityGroup: sg1 })); - const conn2 = new SomethingConnectable(new Connections({ securityGroup: sg2 })); - - // WHEN - conn1.connections.allowTo(conn2, new TcpPort(80), 'Test'); + connections1.allowTo(connectable, new TcpPort(88)); + connections2.addSecurityGroup(sg3); // THEN - test.done(); - }, - - '(imported) SecurityGroup can be used as target of .allowTo()'(test: Test) { - // GIVEN - const stack = new Stack(); - const vpc = new VpcNetwork(stack, 'VPC'); - const sg1 = new SecurityGroup(stack, 'SomeSecurityGroup', { vpc, allowAllOutbound: false }); - const somethingConnectable = new SomethingConnectable(new Connections({ securityGroup: sg1 })); - - const securityGroup = SecurityGroupRef.import(stack, 'ImportedSG', { securityGroupId: 'sg-12345' }); - - // WHEN - somethingConnectable.connections.allowTo(securityGroup, new TcpAllPorts(), 'Connect there'); - - // THEN: rule to generated security group to connect to imported - expect(stack).to(haveResource("AWS::EC2::SecurityGroupEgress", { - GroupId: { "Fn::GetAtt": [ "SomeSecurityGroupEF219AD6", "GroupId" ] }, - IpProtocol: "tcp", - Description: "Connect there", - DestinationSecurityGroupId: "sg-12345", - FromPort: 0, - ToPort: 65535 + expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + GroupId: { "Fn::GetAtt": [ "SecurityGroup23BE86BB7", "GroupId" ] }, + SourceSecurityGroupId: { "Fn::GetAtt": [ "SecurityGroup1F554B36F", "GroupId" ] }, + FromPort: 88, + ToPort: 88 })); - // THEN: rule to imported security group to allow connections from generated - expect(stack).to(haveResource("AWS::EC2::SecurityGroupIngress", { - IpProtocol: "tcp", - Description: "Connect there", - FromPort: 0, - GroupId: "sg-12345", - SourceSecurityGroupId: { "Fn::GetAtt": [ "SomeSecurityGroupEF219AD6", "GroupId" ] }, - ToPort: 65535 + expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + GroupId: { "Fn::GetAtt": [ "SecurityGroup3E5E374B9", "GroupId" ] }, + SourceSecurityGroupId: { "Fn::GetAtt": [ "SecurityGroup1F554B36F", "GroupId" ] }, + FromPort: 88, + ToPort: 88 })); test.done(); }, - 'peer between all types of peers and port range types'(test: Test) { + 'multiple security groups allows internally between them'(test: Test) { // GIVEN - const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' }}); + const stack = new Stack(); const vpc = new VpcNetwork(stack, 'VPC'); - const sg = new SecurityGroup(stack, 'SG', { vpc }); - - const peers = [ - new SecurityGroup(stack, 'PeerGroup', { vpc }), - new AnyIPv4(), - new AnyIPv6(), - new PrefixList('pl-012345'), - ]; - - const ports = [ - new TcpPort(1234), - new TcpPortFromAttribute("tcp-test-port!"), - new TcpAllPorts(), - new TcpPortRange(80, 90), - new UdpPort(2345), - new UdpPortFromAttribute("udp-test-port!"), - new UdpAllPorts(), - new UdpPortRange(85, 95), - new IcmpTypeAndCode(5, 1), - new IcmpAllTypeCodes(8), - new IcmpAllTypesAndCodes(), - new IcmpPing(), - new AllTraffic() - ]; + const sg1 = new SecurityGroup(stack, 'SecurityGroup1', { vpc, allowAllOutbound: false }); + const sg2 = new SecurityGroup(stack, 'SecurityGroup2', { vpc, allowAllOutbound: false }); + const connections = new Connections({ securityGroups: [sg1] }); // WHEN - for (const peer of peers) { - for (const port of ports) { - sg.connections.allowTo(peer, port); - } - } + connections.allowInternally(new TcpPort(88)); + connections.addSecurityGroup(sg2); - // THEN -- no crash + // THEN + expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + GroupId: { "Fn::GetAtt": [ "SecurityGroup1F554B36F", "GroupId" ] }, + SourceSecurityGroupId: { "Fn::GetAtt": [ "SecurityGroup1F554B36F", "GroupId" ] }, + FromPort: 88, + ToPort: 88 + })); test.done(); - } + }, }; class SomethingConnectable implements IConnectable { diff --git a/packages/@aws-cdk/aws-ec2/test/test.security-group.ts b/packages/@aws-cdk/aws-ec2/test/test.security-group.ts new file mode 100644 index 0000000000000..0219f16350f7f --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/test.security-group.ts @@ -0,0 +1,175 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/cdk'; +import { Test } from 'nodeunit'; + +import { + AllTraffic, + AnyIPv4, + AnyIPv6, + IcmpAllTypeCodes, + IcmpAllTypesAndCodes, + IcmpPing, + IcmpTypeAndCode, + PrefixList, + SecurityGroup, + TcpAllPorts, + TcpPort, + TcpPortFromAttribute, + TcpPortRange, + UdpAllPorts, + UdpPort, + UdpPortFromAttribute, + UdpPortRange, + VpcNetwork +} from "../lib"; + +export = { + 'security group can allows all outbound traffic by default'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new VpcNetwork(stack, 'VPC'); + + // WHEN + new SecurityGroup(stack, 'SG1', { vpc, allowAllOutbound: true }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: "0.0.0.0/0", + Description: "Allow all outbound traffic by default", + IpProtocol: "-1" + } + ], + })); + + test.done(); + }, + + 'no new outbound rule is added if we are allowing all traffic anyway'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new VpcNetwork(stack, 'VPC'); + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', { vpc, allowAllOutbound: true }); + sg.addEgressRule(new AnyIPv4(), new TcpPort(86), 'This does not show up'); + + // THEN + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: "0.0.0.0/0", + Description: "Allow all outbound traffic by default", + IpProtocol: "-1" + }, + ], + })); + + test.done(); + }, + + 'security group disallow outbound traffic by default'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new VpcNetwork(stack, 'VPC'); + + // WHEN + new SecurityGroup(stack, 'SG1', { vpc, allowAllOutbound: false }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: "255.255.255.255/32", + Description: "Disallow all traffic", + FromPort: 252, + IpProtocol: "icmp", + ToPort: 86 + } + ], + })); + + test.done(); + }, + + 'bogus outbound rule disappears if another rule is added'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new VpcNetwork(stack, 'VPC'); + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', { vpc, allowAllOutbound: false }); + sg.addEgressRule(new AnyIPv4(), new TcpPort(86), 'This replaces the other one'); + + // THEN + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: "0.0.0.0/0", + Description: "This replaces the other one", + FromPort: 86, + IpProtocol: "tcp", + ToPort: 86 + } + ], + })); + + test.done(); + }, + + 'all outbound rule cannot be added after creation'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new VpcNetwork(stack, 'VPC'); + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', { vpc, allowAllOutbound: false }); + test.throws(() => { + sg.addEgressRule(new AnyIPv4(), new AllTraffic(), 'All traffic'); + }, /Cannot add/); + + test.done(); + }, + + 'peer between all types of peers and port range types'(test: Test) { + // GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' }}); + const vpc = new VpcNetwork(stack, 'VPC'); + const sg = new SecurityGroup(stack, 'SG', { vpc }); + + const peers = [ + new SecurityGroup(stack, 'PeerGroup', { vpc }), + new AnyIPv4(), + new AnyIPv6(), + new PrefixList('pl-012345'), + ]; + + const ports = [ + new TcpPort(1234), + new TcpPortFromAttribute("tcp-test-port!"), + new TcpAllPorts(), + new TcpPortRange(80, 90), + new UdpPort(2345), + new UdpPortFromAttribute("udp-test-port!"), + new UdpAllPorts(), + new UdpPortRange(85, 95), + new IcmpTypeAndCode(5, 1), + new IcmpAllTypeCodes(8), + new IcmpAllTypesAndCodes(), + new IcmpPing(), + new AllTraffic() + ]; + + // WHEN + for (const peer of peers) { + for (const port of ports) { + sg.connections.allowTo(peer, port); + } + } + + // THEN -- no crash + + test.done(); + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 819f063976607..323372ec07d65 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -4,11 +4,15 @@ This package contains constructs for working with **AWS Elastic Container Service** (ECS). The simplest example of using this library looks like this: ```ts -// Create an ECS cluster (backed by an AutoScaling group) +// Create an ECS cluster const cluster = new ecs.Ec2Cluster(this, 'Cluster', { vpc, - size: 3, - instanceType: new InstanceType("t2.xlarge") +}); + +// Add capacity to it +cluster.addDefaultAutoScalingGroupCapacity({ + instanceType: new InstanceType("t2.xlarge"), + instanceCount: 3, }); // Instantiate ECS Service with an automatic load balancer @@ -55,7 +59,7 @@ running the latest ECS Optimized AMI will automatically be created for you. You can run many tasks on a single cluster. -To create a cluster, go: +To create a Fargate cluster, go: ```ts const cluster = new ecs.FargateCluster(this, 'Cluster', { @@ -63,6 +67,32 @@ const cluster = new ecs.FargateCluster(this, 'Cluster', { }); ``` +If you decide to create an EC2 cluster, don't forget to add +capacity to it, in one of the following ways: + +```ts +const cluster = new ecs.Ec2Cluster(this, 'Cluster', { + vpc: vpc +}); + +// Either add default capacity +cluster.addDefaultAutoScalingGroupCapacity({ + instanceType: new ec2.InstanceType("t2.xlarge"), + instanceCount: 3, +}); + +// Or add customized capacity. Be sure to start the ECS-optimized AMI. +const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'ASG', { + vpc, + instanceType: new ec2.InstanceType('t2.xlarge'), + machineImage: new EcsOptimizedAmi(), + desiredCapacity: 3, + // ... other options here ... +}); + +cluster.addAutoScalingGroupCapacity(autoScalingGroup); +``` + ## TaskDefinition A `TaskDefinition` describes what a single copy of a **Task** should look like. diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 9fc97a7a5becc..19aa1b9c9fad5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -66,7 +66,7 @@ export abstract class BaseService extends cdk.Construct /** * Manage allowed network traffic for this service */ - public abstract readonly connections: ec2.Connections; + public readonly connections: ec2.Connections = new ec2.Connections(); /** * ARN of this service @@ -86,7 +86,6 @@ export abstract class BaseService extends cdk.Construct protected loadBalancers = new Array(); protected networkConfiguration?: cloudformation.ServiceResource.NetworkConfigurationProperty; protected readonly abstract taskDef: BaseTaskDefinition; - protected _securityGroup?: ec2.SecurityGroupRef; private readonly resource: cloudformation.ServiceResource; private scalableTaskCount?: ScalableTaskCount; @@ -139,13 +138,6 @@ export abstract class BaseService extends cdk.Construct return this.attachToELBv2(targetGroup); } - /** - * SecurityGroup of this service - */ - public get securityGroup(): ec2.SecurityGroupRef { - return this._securityGroup!; - } - /** * Enable autoscaling for the number of tasks in this service */ @@ -187,7 +179,7 @@ export abstract class BaseService extends cdk.Construct securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc }); } const subnets = vpc.subnets(vpcPlacement); - this._securityGroup = securityGroup; + this.connections.addSecurityGroup(securityGroup); this.networkConfiguration = { awsvpcConfiguration: { diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts index 3a55644d70c60..86638fa95e63b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts @@ -8,25 +8,8 @@ import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; /** * Properties to define an ECS cluster */ +// tslint:disable-next-line:no-empty-interface export interface Ec2ClusterProps extends BaseClusterProps { - /** - * Whether or not the containers can access the instance role - * - * @default false - */ - containersAccessInstanceRole?: boolean; - - /** - * The type of EC2 instance to launch into your Autoscaling Group - */ - instanceType?: ec2.InstanceType; - - /** - * Number of container instances registered in your ECS Cluster - * - * @default 1 - */ - size?: number; } /** @@ -41,34 +24,41 @@ export class Ec2Cluster extends BaseCluster implements IEc2Cluster { } /** - * The AutoScalingGroup that the cluster is running on + * Connections manager for the EC2 cluster */ - public readonly autoScalingGroup: autoscaling.AutoScalingGroup; - - /** - * SecurityGroup of the EC2 instances - */ - public readonly securityGroup: ec2.SecurityGroupRef; + public readonly connections: ec2.Connections = new ec2.Connections(); constructor(parent: cdk.Construct, name: string, props: Ec2ClusterProps) { super(parent, name, props); + } - const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'AutoScalingGroup', { - vpc: props.vpc, - instanceType: props.instanceType || new ec2.InstanceTypePair(ec2.InstanceClass.T2, ec2.InstanceSize.Micro), + /** + * Add a default-configured AutoScalingGroup running the ECS-optimized AMI to this Cluster + */ + public addDefaultAutoScalingGroupCapacity(options: AddDefaultAutoScalingGroupOptions) { + const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'DefaultAutoScalingGroup', { + vpc: this.vpc, + instanceType: options.instanceType, machineImage: new EcsOptimizedAmi(), updateType: autoscaling.UpdateType.ReplacingUpdate, minSize: 0, - maxSize: props.size || 1, - desiredCapacity: props.size || 1 + maxSize: options.instanceCount || 1, + desiredCapacity: options.instanceCount || 1 }); - this.securityGroup = autoScalingGroup.connections.securityGroup!; + this.addAutoScalingGroupCapacity(autoScalingGroup); + } + + /** + * Add compute capacity to this ECS cluster in the form of an AutoScalingGroup + */ + public addAutoScalingGroupCapacity(autoScalingGroup: autoscaling.AutoScalingGroup, options: AddAutoScalingGroupCapacityOptions = {}) { + this.connections.connections.addSecurityGroup(...autoScalingGroup.connections.securityGroups); // Tie instances to cluster autoScalingGroup.addUserData(`echo ECS_CLUSTER=${this.clusterName} >> /etc/ecs/ecs.config`); - if (!props.containersAccessInstanceRole) { + if (!options.containersAccessInstanceRole) { // Deny containers access to instance metadata service // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html autoScalingGroup.addUserData('sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP'); @@ -88,14 +78,9 @@ export class Ec2Cluster extends BaseCluster implements IEc2Cluster { "ecs:StartTelemetrySession", "ecs:Submit*", "ecr:GetAuthorizationToken", - "ecr:BatchCheckLayerAvailability", - "ecr:GetDownloadUrlForLayer", - "ecr:BatchGetImage", "logs:CreateLogStream", "logs:PutLogEvents" - ).addAllResources()); // Conceivably we might do better than all resources and add targeted ARNs - - this.autoScalingGroup = autoScalingGroup; + ).addAllResources()); } /** @@ -105,7 +90,7 @@ export class Ec2Cluster extends BaseCluster implements IEc2Cluster { return { clusterName: new cdk.Output(this, 'ClusterName', { value: this.clusterName }).makeImportValue().toString(), vpc: this.vpc.export(), - securityGroup: this.securityGroup.export(), + securityGroups: this.connections.securityGroups.map(sg => sg.export()), }; } @@ -176,9 +161,9 @@ export interface IEc2Cluster { readonly vpc: ec2.VpcNetworkRef; /** - * Security group of the cluster instances + * Connections manager of the cluster instances */ - readonly securityGroup: ec2.SecurityGroupRef; + readonly connections: ec2.Connections; } /** @@ -198,7 +183,7 @@ export interface ImportedEc2ClusterProps { /** * Security group of the cluster instances */ - securityGroup: ec2.SecurityGroupRefProps; + securityGroups: ec2.SecurityGroupRefProps[]; } /** @@ -218,12 +203,47 @@ class ImportedEc2Cluster extends cdk.Construct implements IEc2Cluster { /** * Security group of the cluster instances */ - public readonly securityGroup: ec2.SecurityGroupRef; + public readonly connections = new ec2.Connections(); constructor(parent: cdk.Construct, name: string, props: ImportedEc2ClusterProps) { super(parent, name); this.clusterName = props.clusterName; this.vpc = ec2.VpcNetworkRef.import(this, "vpc", props.vpc); - this.securityGroup = ec2.SecurityGroupRef.import(this, "securityGroup", props.securityGroup); + + let i = 1; + for (const sgProps of props.securityGroups) { + this.connections.addSecurityGroup(ec2.SecurityGroupRef.import(this, `SecurityGroup${i}`, sgProps)); + i++; + } } } + +/** + * Properties for adding an autoScalingGroup + */ +export interface AddAutoScalingGroupCapacityOptions { + /** + * Whether or not the containers can access the instance role + * + * @default false + */ + containersAccessInstanceRole?: boolean; +} + +/** + * Properties for adding autoScalingGroup + */ +export interface AddDefaultAutoScalingGroupOptions { + + /** + * The type of EC2 instance to launch into your Autoscaling Group + */ + instanceType: ec2.InstanceType; + + /** + * Number of container instances registered in your ECS Cluster + * + * @default 1 + */ + instanceCount?: number; +} diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 7a685378f4c61..8a35bcc954570 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -62,11 +62,6 @@ export interface Ec2ServiceProps extends BaseServiceProps { * Start a service on an ECS cluster */ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { - /** - * Manage allowed network traffic for this construct - */ - public readonly connections: ec2.Connections; - /** * Name of the cluster */ @@ -103,10 +98,9 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { } else { // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. validateNoNetworkingProps(props); - this._securityGroup = props.cluster.securityGroup!; + this.connections.addSecurityGroup(...props.cluster.connections.securityGroups); } - this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); this.taskDefinition = props.taskDefinition; this.taskDef = props.taskDefinition; @@ -284,4 +278,4 @@ export enum BinPackResource { * Fill up hosts' memory allocations first */ Memory = 'memory', -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 96ee71e243beb..1ecc32a7ce548 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -55,11 +55,6 @@ export interface FargateServiceProps extends BaseServiceProps { * Start a service on an ECS cluster */ export class FargateService extends BaseService { - /** - * Manage allowed network traffic for this construct - */ - public readonly connections: ec2.Connections; - /** * The Task Definition for this service */ @@ -75,7 +70,6 @@ export class FargateService extends BaseService { }, props.cluster.clusterName); this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcPlacement, props.securityGroup); - this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); if (!props.taskDefinition.defaultContainer) { throw new Error('A TaskDefinition must have at least one essential container'); @@ -117,4 +111,4 @@ export enum FargatePlatformVersion { * Based on Amazon Linux 2017.09. */ Version1_0 = '1.0.0', -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json index 7fe6647326286..577349b090e32 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json @@ -308,10 +308,10 @@ "Ec2ClusterEE43E89D": { "Type": "AWS::ECS::Cluster" }, - "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37": { + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E": { "Type": "AWS::EC2::SecurityGroup", "Properties": { - "GroupDescription": "aws-ecs-integ/Ec2Cluster/AutoScalingGroup/InstanceSecurityGroup", + "GroupDescription": "aws-ecs-integ/Ec2Cluster/DefaultAutoScalingGroup/InstanceSecurityGroup", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", @@ -323,7 +323,7 @@ "Tags": [ { "Key": "Name", - "Value": "aws-ecs-integ/Ec2Cluster/AutoScalingGroup" + "Value": "aws-ecs-integ/Ec2Cluster/DefaultAutoScalingGroup" } ], "VpcId": { @@ -331,7 +331,7 @@ } } }, - "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C": { + "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -348,7 +348,7 @@ } } }, - "Ec2ClusterAutoScalingGroupInstanceRoleDefaultPolicyD987CC12": { + "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -363,9 +363,6 @@ "ecs:StartTelemetrySession", "ecs:Submit*", "ecr:GetAuthorizationToken", - "ecr:BatchCheckLayerAvailability", - "ecr:GetDownloadUrlForLayer", - "ecr:BatchGetImage", "logs:CreateLogStream", "logs:PutLogEvents" ], @@ -375,36 +372,36 @@ ], "Version": "2012-10-17" }, - "PolicyName": "Ec2ClusterAutoScalingGroupInstanceRoleDefaultPolicyD987CC12", + "PolicyName": "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD", "Roles": [ { - "Ref": "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C" + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" } ] } }, - "Ec2ClusterAutoScalingGroupInstanceProfile18B9D870": { + "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471": { "Type": "AWS::IAM::InstanceProfile", "Properties": { "Roles": [ { - "Ref": "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C" + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" } ] } }, - "Ec2ClusterAutoScalingGroupLaunchConfig570E562A": { + "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { "ImageId": "ami-1234", "InstanceType": "t2.micro", "IamInstanceProfile": { - "Ref": "Ec2ClusterAutoScalingGroupInstanceProfile18B9D870" + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471" }, "SecurityGroups": [ { "Fn::GetAtt": [ - "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37", + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", "GroupId" ] } @@ -425,24 +422,24 @@ } }, "DependsOn": [ - "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C", - "Ec2ClusterAutoScalingGroupInstanceRoleDefaultPolicyD987CC12" + "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898", + "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD" ] }, - "Ec2ClusterAutoScalingGroupASGC290B9EA": { + "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { "MaxSize": "1", "MinSize": "0", "DesiredCapacity": "1", "LaunchConfigurationName": { - "Ref": "Ec2ClusterAutoScalingGroupLaunchConfig570E562A" + "Ref": "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A" }, "Tags": [ { "Key": "Name", "PropagateAtLaunch": true, - "Value": "aws-ecs-integ/Ec2Cluster/AutoScalingGroup" + "Value": "aws-ecs-integ/Ec2Cluster/DefaultAutoScalingGroup" } ], "VPCZoneIdentifier": [ @@ -725,4 +722,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts index 62f7f86a52ea0..c61b06a7c338d 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts @@ -10,6 +10,9 @@ const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); +cluster.addDefaultAutoScalingGroupCapacity({ + instanceType: new ec2.InstanceType('t2.micro') +}); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: NetworkMode.AwsVpc diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json index bf22690292b47..828607c0abf3d 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json @@ -308,10 +308,10 @@ "Ec2ClusterEE43E89D": { "Type": "AWS::ECS::Cluster" }, - "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37": { + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E": { "Type": "AWS::EC2::SecurityGroup", "Properties": { - "GroupDescription": "aws-ecs-integ-ecs/Ec2Cluster/AutoScalingGroup/InstanceSecurityGroup", + "GroupDescription": "aws-ecs-integ-ecs/Ec2Cluster/DefaultAutoScalingGroup/InstanceSecurityGroup", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", @@ -323,7 +323,7 @@ "Tags": [ { "Key": "Name", - "Value": "aws-ecs-integ-ecs/Ec2Cluster/AutoScalingGroup" + "Value": "aws-ecs-integ-ecs/Ec2Cluster/DefaultAutoScalingGroup" } ], "VpcId": { @@ -331,7 +331,7 @@ } } }, - "Ec2ClusterAutoScalingGroupInstanceSecurityGroupfromawsecsintegecsLBSecurityGroup7DA9012980802D795A20": { + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroupfromawsecsintegecsLBSecurityGroup7DA90129808025D51C31": { "Type": "AWS::EC2::SecurityGroupIngress", "Properties": { "IpProtocol": "tcp", @@ -339,7 +339,7 @@ "FromPort": 8080, "GroupId": { "Fn::GetAtt": [ - "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37", + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", "GroupId" ] }, @@ -352,7 +352,7 @@ "ToPort": 8080 } }, - "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C": { + "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -369,7 +369,7 @@ } } }, - "Ec2ClusterAutoScalingGroupInstanceRoleDefaultPolicyD987CC12": { + "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -384,9 +384,6 @@ "ecs:StartTelemetrySession", "ecs:Submit*", "ecr:GetAuthorizationToken", - "ecr:BatchCheckLayerAvailability", - "ecr:GetDownloadUrlForLayer", - "ecr:BatchGetImage", "logs:CreateLogStream", "logs:PutLogEvents" ], @@ -396,36 +393,36 @@ ], "Version": "2012-10-17" }, - "PolicyName": "Ec2ClusterAutoScalingGroupInstanceRoleDefaultPolicyD987CC12", + "PolicyName": "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD", "Roles": [ { - "Ref": "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C" + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" } ] } }, - "Ec2ClusterAutoScalingGroupInstanceProfile18B9D870": { + "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471": { "Type": "AWS::IAM::InstanceProfile", "Properties": { "Roles": [ { - "Ref": "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C" + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" } ] } }, - "Ec2ClusterAutoScalingGroupLaunchConfig570E562A": { + "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { "ImageId": "ami-1234", "InstanceType": "t2.micro", "IamInstanceProfile": { - "Ref": "Ec2ClusterAutoScalingGroupInstanceProfile18B9D870" + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471" }, "SecurityGroups": [ { "Fn::GetAtt": [ - "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37", + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", "GroupId" ] } @@ -446,24 +443,24 @@ } }, "DependsOn": [ - "Ec2ClusterAutoScalingGroupInstanceRole99A1E97C", - "Ec2ClusterAutoScalingGroupInstanceRoleDefaultPolicyD987CC12" + "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898", + "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD" ] }, - "Ec2ClusterAutoScalingGroupASGC290B9EA": { + "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { "MaxSize": "1", "MinSize": "0", "DesiredCapacity": "1", "LaunchConfigurationName": { - "Ref": "Ec2ClusterAutoScalingGroupLaunchConfig570E562A" + "Ref": "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A" }, "Tags": [ { "Key": "Name", "PropagateAtLaunch": true, - "Value": "aws-ecs-integ-ecs/Ec2Cluster/AutoScalingGroup" + "Value": "aws-ecs-integ-ecs/Ec2Cluster/DefaultAutoScalingGroup" } ], "VPCZoneIdentifier": [ @@ -621,7 +618,7 @@ } } }, - "LBSecurityGrouptoawsecsintegecsEc2ClusterAutoScalingGroupInstanceSecurityGroup4939BA238080198BB235": { + "LBSecurityGrouptoawsecsintegecsEc2ClusterDefaultAutoScalingGroupInstanceSecurityGroup03E2DA498080B03BF019": { "Type": "AWS::EC2::SecurityGroupEgress", "Properties": { "GroupId": { @@ -634,7 +631,7 @@ "Description": "Load balancer to target", "DestinationSecurityGroupId": { "Fn::GetAtt": [ - "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37", + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", "GroupId" ] }, @@ -688,4 +685,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts index 31bb39c6220ae..8c00625228206 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts @@ -10,6 +10,9 @@ const stack = new cdk.Stack(app, 'aws-ecs-integ-ecs'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); +cluster.addDefaultAutoScalingGroupCapacity({ + instanceType: new ec2.InstanceType('t2.micro') +}); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { // networkMode defaults to "bridge" diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts index 126a030bbd80d..fab9632d2a261 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts @@ -11,10 +11,14 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - new ecs.Ec2Cluster(stack, 'Ec2Cluster', { + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc, }); + cluster.addDefaultAutoScalingGroupCapacity({ + instanceType: new ec2.InstanceType('t2.micro') + }); + expect(stack).to(haveResource("AWS::ECS::Cluster")); expect(stack).to(haveResource("AWS::EC2::VPC", { @@ -34,12 +38,12 @@ export = { ImageId: "", // Should this not be the latest image ID? InstanceType: "t2.micro", IamInstanceProfile: { - Ref: "Ec2ClusterAutoScalingGroupInstanceProfile18B9D870" + Ref: "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471" }, SecurityGroups: [ { "Fn::GetAtt": [ - "Ec2ClusterAutoScalingGroupInstanceSecurityGroup77BA7E37", + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", "GroupId" ] } @@ -66,13 +70,13 @@ export = { MinSize: "0", DesiredCapacity: "1", LaunchConfigurationName: { - Ref: "Ec2ClusterAutoScalingGroupLaunchConfig570E562A" + Ref: "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A" }, Tags: [ { Key: "Name", PropagateAtLaunch: true, - Value: "Ec2Cluster/AutoScalingGroup" + Value: "Ec2Cluster/DefaultAutoScalingGroup" } ], VPCZoneIdentifier: [ @@ -89,7 +93,7 @@ export = { })); expect(stack).to(haveResource("AWS::EC2::SecurityGroup", { - GroupDescription: "Ec2Cluster/AutoScalingGroup/InstanceSecurityGroup", + GroupDescription: "Ec2Cluster/DefaultAutoScalingGroup/InstanceSecurityGroup", SecurityGroupEgress: [ { CidrIp: "0.0.0.0/0", @@ -101,7 +105,7 @@ export = { Tags: [ { Key: "Name", - Value: "Ec2Cluster/AutoScalingGroup" + Value: "Ec2Cluster/DefaultAutoScalingGroup" } ], VpcId: { @@ -125,29 +129,26 @@ export = { })); expect(stack).to(haveResource("AWS::IAM::Policy", { - PolicyDocument: { - Statement: [ - { - Action: [ - "ecs:CreateCluster", - "ecs:DeregisterContainerInstance", - "ecs:DiscoverPollEndpoint", - "ecs:Poll", - "ecs:RegisterContainerInstance", - "ecs:StartTelemetrySession", - "ecs:Submit*", - "ecr:GetAuthorizationToken", - "ecr:BatchCheckLayerAvailability", - "ecr:GetDownloadUrlForLayer", - "ecr:BatchGetImage", - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - Effect: "Allow", - Resource: "*" - } - ], - Version: "2012-10-17" + PolicyDocument: { + Statement: [ + { + Action: [ + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Effect: "Allow", + Resource: "*" + } + ], + Version: "2012-10-17" } })); @@ -160,8 +161,8 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - new ecs.Ec2Cluster(stack, 'Ec2Cluster', { - vpc, + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new InstanceType("m3.large") }); @@ -178,9 +179,10 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - new ecs.Ec2Cluster(stack, 'Ec2Cluster', { - vpc, - size: 3 + const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ + instanceType: new ec2.InstanceType('t2.micro'), + instanceCount: 3 }); // THEN @@ -190,4 +192,4 @@ export = { test.done(); }, -}; \ No newline at end of file +}; diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts index 7df533982bab5..3fc0f35b46c70 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts @@ -208,7 +208,7 @@ export class LoadBalancer extends cdk.Construct implements IConnectable, codedep super(parent, name); this.securityGroup = new SecurityGroup(this, 'SecurityGroup', { vpc: props.vpc, allowAllOutbound: false }); - this.connections = new Connections({ securityGroup: this.securityGroup }); + this.connections = new Connections({ securityGroups: [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; @@ -342,7 +342,7 @@ export class ListenerPort implements IConnectable { public readonly connections: Connections; constructor(securityGroup: SecurityGroupRef, defaultPortRange: IPortRange) { - this.connections = new Connections({ securityGroup, defaultPortRange }); + this.connections = new Connections({ securityGroups: [securityGroup] , defaultPortRange }); } } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 217db667db327..114f499de8ef7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -128,7 +128,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis // This listener edits the securitygroup of the load balancer, // but adds its own default port. this.connections = new ec2.Connections({ - securityGroup: props.loadBalancer.connections.securityGroup, + securityGroups: props.loadBalancer.connections.securityGroups, defaultPortRange: new ec2.TcpPort(port), }); @@ -241,7 +241,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis public export(): ApplicationListenerRefProps { return { listenerArn: new cdk.Output(this, 'ListenerArn', { value: this.listenerArn }).makeImportValue().toString(), - securityGroupId: this.connections.securityGroup!.export().securityGroupId, + securityGroupId: this.connections.securityGroups[0]!.export().securityGroupId, defaultPort: new cdk.Output(this, 'Port', { value: this.defaultPort }).makeImportValue().toString(), }; } @@ -335,7 +335,7 @@ class ImportedApplicationListener extends cdk.Construct implements IApplicationL const defaultPortRange = props.defaultPort !== undefined ? new ec2.TcpPortFromAttribute(props.defaultPort) : undefined; this.connections = new ec2.Connections({ - securityGroup: ec2.SecurityGroupRef.import(this, 'SecurityGroup', { securityGroupId: props.securityGroupId }), + securityGroups: [ec2.SecurityGroupRef.import(this, 'SecurityGroup', { securityGroupId: props.securityGroupId })], defaultPortRange, }); } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index bb4e15ef8ce22..357551789ff7d 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -67,7 +67,7 @@ export class ApplicationLoadBalancer extends BaseLoadBalancer implements IApplic description: `Automatically created Security Group for ELB ${this.uniqueId}`, allowAllOutbound: false }); - this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); + this.connections = new ec2.Connections({ securityGroups: [this.securityGroup] }); if (props.http2Enabled === false) { this.setAttribute('routing.http2.enabled', 'false'); } if (props.idleTimeoutSecs !== undefined) { this.setAttribute('idle_timeout.timeout_seconds', props.idleTimeoutSecs.toString()); } @@ -201,7 +201,7 @@ class ImportedApplicationLoadBalancer extends cdk.Construct implements IApplicat this.loadBalancerArn = props.loadBalancerArn; this.connections = new ec2.Connections({ - securityGroup: ec2.SecurityGroupRef.import(this, 'SecurityGroup', { securityGroupId: props.securityGroupId }) + securityGroups: [ec2.SecurityGroupRef.import(this, 'SecurityGroup', { securityGroupId: props.securityGroupId })] }); } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts index 77fec08c04824..2b129f97c67e7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/helpers.ts @@ -11,7 +11,7 @@ export class FakeSelfRegisteringTarget extends cdk.Construct implements elbv2.IA super(parent, id); this.securityGroup = new ec2.SecurityGroup(this, 'SG', { vpc }); this.connections = new ec2.Connections({ - securityGroup: this.securityGroup + securityGroups: [this.securityGroup] }); } diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index 8b81a01d9cc32..f915ad9922c81 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts @@ -334,8 +334,8 @@ export abstract class FunctionRef extends cdk.Construct public export(): FunctionRefProps { return { functionArn: new cdk.Output(this, 'FunctionArn', { value: this.functionArn }).makeImportValue().toString(), - securityGroupId: this._connections && this._connections.securityGroup - ? new cdk.Output(this, 'SecurityGroupId', { value: this._connections.securityGroup.securityGroupId }).makeImportValue().toString() + securityGroupId: this._connections && this._connections.securityGroups[0] + ? new cdk.Output(this, 'SecurityGroupId', { value: this._connections.securityGroups[0].securityGroupId }).makeImportValue().toString() : undefined }; } @@ -427,9 +427,9 @@ class LambdaRefImport extends FunctionRef { if (props.securityGroupId) { this._connections = new ec2.Connections({ - securityGroup: ec2.SecurityGroupRef.import(this, 'SecurityGroup', { + securityGroups: [ec2.SecurityGroupRef.import(this, 'SecurityGroup', { securityGroupId: props.securityGroupId - }) + })] }); } } @@ -449,6 +449,5 @@ class LambdaRefImport extends FunctionRef { */ private extractNameFromArn(arn: string) { return new cdk.FnSelect(6, new cdk.FnSplit(':', arn)).toString(); - } } diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda.ts b/packages/@aws-cdk/aws-lambda/lib/lambda.ts index ada7082d963bd..e7760707f5369 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda.ts @@ -338,7 +338,7 @@ export class Function extends FunctionRef { allowAllOutbound: props.allowAllOutbound }); - this._connections = new ec2.Connections({ securityGroup }); + this._connections = new ec2.Connections({ securityGroups: [securityGroup] }); // Pick subnets, make sure they're not Public. Routing through an IGW // won't work because the ENIs don't get a Public IP. diff --git a/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts index 2af214ae84f0d..cebff1fcf4272 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts @@ -46,7 +46,7 @@ export = { public 'participates in Connections objects'(test: Test) { // GIVEN const securityGroup = new ec2.SecurityGroup(this.stack, 'SomeSecurityGroup', { vpc: this.vpc }); - const somethingConnectable = new SomethingConnectable(new ec2.Connections({ securityGroup })); + const somethingConnectable = new SomethingConnectable(new ec2.Connections({ securityGroups: [securityGroup] })); // WHEN this.lambda.connections.allowTo(somethingConnectable, new ec2.TcpAllPorts(), 'Lambda can call connectable'); @@ -78,7 +78,7 @@ export = { // GIVEN const stack2 = new cdk.Stack(); const securityGroup = new ec2.SecurityGroup(stack2, 'SomeSecurityGroup', { vpc: this.vpc }); - const somethingConnectable = new SomethingConnectable(new ec2.Connections({ securityGroup })); + const somethingConnectable = new SomethingConnectable(new ec2.Connections({ securityGroups: [securityGroup] })); // WHEN const importedLambda = lambda.FunctionRef.import(stack2, 'Lambda', this.lambda.export()); diff --git a/packages/@aws-cdk/aws-quickstarts/lib/database.ts b/packages/@aws-cdk/aws-quickstarts/lib/database.ts index 3426a68952b3f..69d582b48afda 100644 --- a/packages/@aws-cdk/aws-quickstarts/lib/database.ts +++ b/packages/@aws-cdk/aws-quickstarts/lib/database.ts @@ -49,6 +49,6 @@ export class SqlServer extends cdk.Construct implements ec2.IConnectable { }); const defaultPortRange = new ec2.TcpPort(SqlServer.PORT); - this.connections = new ec2.Connections({ securityGroup, defaultPortRange }); + this.connections = new ec2.Connections({ securityGroups: [securityGroup], defaultPortRange }); } } diff --git a/packages/@aws-cdk/aws-quickstarts/lib/rdgw.ts b/packages/@aws-cdk/aws-quickstarts/lib/rdgw.ts index 156ddb645dc17..54b9bc688e9d2 100644 --- a/packages/@aws-cdk/aws-quickstarts/lib/rdgw.ts +++ b/packages/@aws-cdk/aws-quickstarts/lib/rdgw.ts @@ -52,6 +52,6 @@ export class RemoteDesktopGateway extends cdk.Construct implements ec2.IConnecta }); const defaultPortRange = new ec2.TcpPort(RemoteDesktopGateway.PORT); - this.connections = new ec2.Connections({ securityGroup, defaultPortRange }); + this.connections = new ec2.Connections({ securityGroups: [securityGroup], defaultPortRange }); } } diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts b/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts index 427591f54d38e..5f7d6409a87ee 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts @@ -156,7 +156,7 @@ class ImportedDatabaseCluster extends DatabaseClusterRef { this.securityGroupId = props.securityGroupId; this.defaultPortRange = new ec2.TcpPortFromAttribute(props.port); this.connections = new ec2.Connections({ - securityGroup: ec2.SecurityGroupRef.import(this, 'SecurityGroup', props), + securityGroups: [ec2.SecurityGroupRef.import(this, 'SecurityGroup', props)], defaultPortRange: this.defaultPortRange }); this.clusterIdentifier = props.clusterIdentifier; diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 888900a903302..7b34d104c4a20 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -213,7 +213,7 @@ export class DatabaseCluster extends DatabaseClusterRef { } const defaultPortRange = new ec2.TcpPortFromAttribute(this.clusterEndpoint.port); - this.connections = new ec2.Connections({ securityGroup, defaultPortRange }); + this.connections = new ec2.Connections({ securityGroups: [securityGroup], defaultPortRange }); } } From d8834322dfaa54bff954410faf1120ef97549a4a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Sun, 4 Nov 2018 22:23:17 +0100 Subject: [PATCH 129/140] Fix rename of members on ScalableTaskCount --- packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts b/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts index 564058072d712..1796f7c1d2443 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/scalable-task-count.ts @@ -8,14 +8,14 @@ export class ScalableTaskCount extends appscaling.BaseScalableAttribute { /** * Scale out or in based on time */ - public doScaleOnSchedule(id: string, props: appscaling.ScalingSchedule) { + public scaleOnSchedule(id: string, props: appscaling.ScalingSchedule) { return super.doScaleOnSchedule(id, props); } /** * Scale out or in based on a metric value */ - public doScaleOnMetric(id: string, props: appscaling.BasicStepScalingPolicyProps) { + public scaleOnMetric(id: string, props: appscaling.BasicStepScalingPolicyProps) { return super.doScaleOnMetric(id, props); } @@ -100,4 +100,4 @@ export interface TrackCustomMetricProps extends appscaling.BaseTargetTrackingPro * The target value to achieve for the metric */ targetValue: number; -} \ No newline at end of file +} From edd4d5f402e89328e13e6bd23f0cb1b0a36c43c3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 5 Nov 2018 14:14:10 +0100 Subject: [PATCH 130/140] Make a generic TaskDefinition for both launch types --- packages/@aws-cdk/aws-ecs/README.md | 60 ++- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 27 +- .../aws-ecs/lib/base/base-task-definition.ts | 221 ---------- .../aws-ecs/lib/base/task-definition.ts | 397 ++++++++++++++++++ .../aws-ecs/lib/container-definition.ts | 6 +- .../@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts | 10 +- .../aws-ecs/lib/ec2/ec2-task-definition.ts | 100 +---- .../aws-ecs/lib/fargate/fargate-service.ts | 12 +- .../lib/fargate/fargate-task-definition.ts | 13 +- packages/@aws-cdk/aws-ecs/lib/index.ts | 2 +- .../test/ec2/test.ec2-task-definition.ts | 2 +- .../fargate/test.fargate-task-definition.ts | 2 +- .../aws-ecs/test/test.task-definition.ts | 25 ++ packages/aws-cdk/lib/api/toolkit-info.ts | 1 + scripts/build-typescript.sh | 2 +- 15 files changed, 507 insertions(+), 373 deletions(-) delete mode 100644 packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts create mode 100644 packages/@aws-cdk/aws-ecs/test/test.task-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 323372ec07d65..db30ed5344652 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -28,10 +28,10 @@ const ecsService = new ecs.LoadBalancedEc2Service(this, 'Service', { There are two sets of constructs in this library; one to run tasks on ECS and one to run Tasks on Fargate. -- Use the `Ec2Cluster`, `Ec2TaskDefinition` and `Ec2Service` constructs to - run tasks on EC2 instances running in your account. -- Use the `FargateCluster`, `FargateTaskDefinition` and `FargateService` - constructs to run tasks on instances that are managed for you by AWS. +- Use the `Ec2Cluster` and `Ec2Service` constructs to run tasks on EC2 + instances running in your account. +- Use the `FargateCluster` and `FargateService` constructs to run tasks on + instances that are managed for you by AWS. Here are the main differences: @@ -50,16 +50,12 @@ For more information on EC2 vs Fargate and networking see the AWS Documentation: [AWS Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html) and [Task Networking](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html). - -## Cluster +### Clusters An `Ec2Cluster` or `FargateCluster` defines the infrastructure to run your -tasks on. If you create an ECS cluster, an AutoScalingGroup of EC2 instances -running the latest ECS Optimized AMI will automatically be created for you. - -You can run many tasks on a single cluster. +tasks on. You can run many tasks on a single cluster. -To create a Fargate cluster, go: +To create a Fargate cluster, backed by machines managed by AWS, go: ```ts const cluster = new ecs.FargateCluster(this, 'Cluster', { @@ -67,8 +63,15 @@ const cluster = new ecs.FargateCluster(this, 'Cluster', { }); ``` -If you decide to create an EC2 cluster, don't forget to add -capacity to it, in one of the following ways: +If you create an ECS cluster you also have to create and add machines to it +to run the tasks scheduled on the cluster. Typically, you will add an +AutoScalingGroup with instances running the latest ECS-optimized AMI to +the cluster. There is a method to build and add such an AutoScalingGroup +automatically, or you can supply a customized AutoScalingGroup that +you construct yourself. It's possible to add multiple AutoScalingGroups +with various instance types if you want to. + +Creating an EC2 cluster and adding capacity to it looks like this: ```ts const cluster = new ecs.Ec2Cluster(this, 'Cluster', { @@ -93,7 +96,7 @@ const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'ASG', { cluster.addAutoScalingGroupCapacity(autoScalingGroup); ``` -## TaskDefinition +### Task definitions A `TaskDefinition` describes what a single copy of a **Task** should look like. A task definition has one or more containers; typically, it has one @@ -102,21 +105,32 @@ to the task definition, and it will be marked *essential*) and optionally some supporting containers which are used to support the main container, doings things like upload logs or metrics to monitoring services. -To add containers to a `TaskDefinition`, call `addContainer()`: +When creating a Task Definition you have to specify what kind of +clusters you intend to run it on: EC2 clusters, Fargate clusters, or +both: ```ts -const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { +const taskDefinition = new ecs.TaskDefinition(this, 'TaskDef', { memoryMiB: '512' - cpu: 256 + cpu: 256, + compatibility: ecs.Compatibility.Ec2AndFargate, }); +``` + +To add containers to a `TaskDefinition`, call `addContainer()`: +```ts taskDefinition.addContainer('main', { // Use an image from DockerHub image: ecs.DockerHub.image('amazon/amazon-ecs-sample') }); ``` +If you're not trying to construct task definitions that can run on multiple +cluster types, you can directly instantiate `Ec2TaskDefinition` or +`FargateTaskDefinition`, classes with a simplified API that only contain +properties relevant for that specific launch type. -### Images +#### Images Images supply the software that runs inside the container. Images can be obtained from either DockerHub or from ECR repositories: @@ -126,7 +140,7 @@ obtained from either DockerHub or from ECR repositories: * `repository.getImage(tag)`: use the given ECR repository as the image to start. -## Service +### Service A `Service` instantiates a `TaskDefinition` on a `Cluster` a given number of times, optionally associating them with a load balancer. Tasks that fail will @@ -142,7 +156,7 @@ const service = new ecs.FargateService(this, 'Service', { }); ``` -### Include a load balancer +#### Include a load balancer `Services` are load balancing targets and can be directly attached to load balancers: @@ -163,7 +177,7 @@ There are two higher-level constructs available which include a load balancer fo * `LoadBalancedFargateService` * `LoadBalancedEc2Service` -## Task AutoScaling +### Task AutoScaling You can configure the task count of a service to match demand. Task AutoScaling is configured by calling `autoScaleTaskCount()`: @@ -178,7 +192,7 @@ scaling.scaleOnCpuUtilization('CpuScaling', { Task AutoScaling is powered by *Application AutoScaling*. Refer to that for more information. -## Instance AutoScaling +### Instance AutoScaling If you're running on Fargate, AWS will manage the physical machines that your containers are running on for you. If you're running an ECS cluster however, @@ -187,7 +201,7 @@ your EC2 instances might fill up as your number of Tasks goes up. To avoid placement errors, you will want to configure AutoScaling for your EC2 instance group so that your instance count scales with demand. -## Roadmap +### Roadmap - [ ] Instance AutoScaling - [ ] Service Discovery Integration diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 19aa1b9c9fad5..2883955def9f6 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -4,7 +4,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; +import { NetworkMode, TaskDefinition } from '../base/task-definition'; import { cloudformation } from '../ecs.generated'; import { ScalableTaskCount } from './scalable-task-count'; @@ -83,15 +83,26 @@ export abstract class BaseService extends cdk.Construct */ public readonly clusterName: string; + /** + * Task definition this service is associated with + */ + public readonly taskDefinition: TaskDefinition; + protected loadBalancers = new Array(); protected networkConfiguration?: cloudformation.ServiceResource.NetworkConfigurationProperty; - protected readonly abstract taskDef: BaseTaskDefinition; private readonly resource: cloudformation.ServiceResource; private scalableTaskCount?: ScalableTaskCount; - constructor(parent: cdk.Construct, name: string, props: BaseServiceProps, additionalProps: any, clusterName: string) { + constructor(parent: cdk.Construct, + name: string, + props: BaseServiceProps, + additionalProps: any, + clusterName: string, + taskDefinition: TaskDefinition) { super(parent, name); + this.taskDefinition = taskDefinition; + this.resource = new cloudformation.ServiceResource(this, "Service", { desiredCount: props.desiredCount || 1, serviceName: props.serviceName, @@ -121,7 +132,7 @@ export abstract class BaseService extends cdk.Construct // Open up security groups. For dynamic port mapping, we won't know the port range // in advance so we need to open up all ports. - const port = this.taskDef.defaultContainer!.ingressPort; + const port = this.taskDefinition.defaultContainer!.ingressPort; const portRange = port === 0 ? EPHEMERAL_PORT_RANGE : new ec2.TcpPort(port); targetGroup.registerConnectable(this, portRange); @@ -194,19 +205,19 @@ export abstract class BaseService extends cdk.Construct * Shared logic for attaching to an ELBv2 */ private attachToELBv2(targetGroup: elbv2.ITargetGroup): elbv2.LoadBalancerTargetProps { - if (this.taskDef.networkMode === NetworkMode.None) { + if (this.taskDefinition.networkMode === NetworkMode.None) { throw new Error("Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead."); } this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, - containerName: this.taskDef.defaultContainer!.id, - containerPort: this.taskDef.defaultContainer!.containerPort, + containerName: this.taskDefinition.defaultContainer!.id, + containerPort: this.taskDefinition.defaultContainer!.containerPort, }); this.resource.addDependency(targetGroup.listenerDependency()); - const targetType = this.taskDef.networkMode === NetworkMode.AwsVpc ? elbv2.TargetType.Ip : elbv2.TargetType.Instance; + const targetType = this.taskDefinition.networkMode === NetworkMode.AwsVpc ? elbv2.TargetType.Ip : elbv2.TargetType.Instance; return { targetType }; } diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts deleted file mode 100644 index 945649ed1fd9f..0000000000000 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-task-definition.ts +++ /dev/null @@ -1,221 +0,0 @@ -import iam = require('@aws-cdk/aws-iam'); -import cdk = require('@aws-cdk/cdk'); -import { ContainerDefinition, ContainerDefinitionProps } from '../container-definition'; -import { cloudformation } from '../ecs.generated'; - -/** - * Basic task definition properties - */ -export interface BaseTaskDefinitionProps { - /** - * Namespace for task definition versions - * - * @default Automatically generated name - */ - family?: string; - - /** - * The IAM role assumed by the ECS agent. - * - * The role will be used to retrieve container images from ECR and - * create CloudWatch log groups. - * - * @default An execution role will be automatically created if you use ECR images in your task definition - */ - executionRole?: iam.Role; - - /** - * The IAM role assumable by your application code running inside the container - * - * @default A task role is automatically created for you - */ - taskRole?: iam.Role; - - /** - * See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide//task_definition_parameters.html#volumes - */ - volumes?: Volume[]; -} - -/** - * Base class for Ecs and Fargate task definitions - */ -export abstract class BaseTaskDefinition extends cdk.Construct { - /** - * The family name of this task definition - */ - public readonly family: string; - - /** - * ARN of this task definition - */ - public readonly taskDefinitionArn: string; - - /** - * Task role used by this task definition - */ - public readonly taskRole: iam.Role; - - /** - * Network mode used by this task definition - */ - public abstract readonly networkMode: NetworkMode; - - /** - * Default container for this task - * - * Load balancers will send traffic to this container. The first - * essential container that is added to this task will become the default - * container. - */ - public defaultContainer?: ContainerDefinition; - - /** - * All containers - */ - protected readonly containers = new Array(); - - /** - * All volumes - */ - private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; - - /** - * Execution role for this task definition - * - * Will be created as needed. - */ - private executionRole?: iam.Role; - - constructor(parent: cdk.Construct, name: string, props: BaseTaskDefinitionProps, additionalProps: any) { - super(parent, name); - - this.family = props.family || this.uniqueId; - - if (props.volumes) { - props.volumes.forEach(v => this.addVolume(v)); - } - - this.executionRole = props.executionRole; - - this.taskRole = props.taskRole || new iam.Role(this, 'TaskRole', { - assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), - }); - - const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { - containerDefinitions: new cdk.Token(() => this.containers.map(x => x.renderContainerDefinition())), - volumes: new cdk.Token(() => this.volumes), - executionRoleArn: new cdk.Token(() => this.executionRole && this.executionRole.roleArn), - family: this.family, - taskRoleArn: this.taskRole.roleArn, - ...additionalProps - }); - - this.taskDefinitionArn = taskDef.taskDefinitionArn; - } - - /** - * Add a policy statement to the Task Role - */ - public addToTaskRolePolicy(statement: iam.PolicyStatement) { - this.taskRole.addToPolicy(statement); - } - - public addToExecutionRolePolicy(statement: iam.PolicyStatement) { - if (!this.executionRole) { - this.executionRole = new iam.Role(this, 'ExecutionRole', { - assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), - }); - } - this.executionRole.addToPolicy(statement); - } - - /** - * Create a new container to this task definition - */ - public addContainer(id: string, props: ContainerDefinitionProps) { - const container = new ContainerDefinition(this, id, this, props); - this.containers.push(container); - if (this.defaultContainer === undefined && container.essential) { - this.defaultContainer = container; - } - - return container; - } - - /** - * Add a volume to this task definition - */ - public addVolume(volume: Volume) { - this.volumes.push(volume); - } -} - -/** - * The Docker networking mode to use for the containers in the task. - */ -export enum NetworkMode { - /** - * The task's containers do not have external connectivity and port mappings can't be specified in the container definition. - */ - None = 'none', - - /** - * The task utilizes Docker's built-in virtual network which runs inside each container instance. - */ - Bridge = 'bridge', - - /** - * The task is allocated an elastic network interface. - */ - AwsVpc = 'awsvpc', - - /** - * The task bypasses Docker's built-in virtual network and maps container ports directly to the EC2 instance's network interface directly. - * - * In this mode, you can't run multiple instantiations of the same task on a - * single container instance when port mappings are used. - */ - Host = 'host', -} - -/** - * Compatibilties - */ -export enum Compatibilities { - /** - * EC2 capabilities - */ - Ec2 = "EC2", - - /** - * Fargate capabilities - */ - Fargate = "FARGATE" -} - -/** - * Volume definition - */ -export interface Volume { - /** - * Path on the host - */ - host?: Host; - - /** - * A name for the volume - */ - name?: string; - // FIXME add dockerVolumeConfiguration -} - -/** - * A volume host - */ -export interface Host { - /** - * Source path on the host - */ - sourcePath?: string; -} diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts new file mode 100644 index 0000000000000..120598e00ef1e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -0,0 +1,397 @@ +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); +import { ContainerDefinition, ContainerDefinitionProps } from '../container-definition'; +import { cloudformation } from '../ecs.generated'; + +/** + * Properties common to all Task definitions + */ +export interface CommonTaskDefinitionProps { + /** + * Namespace for task definition versions + * + * @default Automatically generated name + */ + family?: string; + + /** + * The IAM role assumed by the ECS agent. + * + * The role will be used to retrieve container images from ECR and + * create CloudWatch log groups. + * + * @default An execution role will be automatically created if you use ECR images in your task definition + */ + executionRole?: iam.Role; + + /** + * The IAM role assumable by your application code running inside the container + * + * @default A task role is automatically created for you + */ + taskRole?: iam.Role; + + /** + * See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide//task_definition_parameters.html#volumes + */ + volumes?: Volume[]; +} + +/** + * Properties for generic task definitions + */ +export interface TaskDefinitionProps extends CommonTaskDefinitionProps { + /** + * The Docker networking mode to use for the containers in the task. + * + * On Fargate, the only supported networking mode is AwsVpc. + * + * @default NetworkMode.Bridge for EC2 tasks, AwsVpc for Fargate tasks. + */ + networkMode?: NetworkMode; + + /** + * An array of placement constraint objects to use for the task. You can + * specify a maximum of 10 constraints per task (this limit includes + * constraints in the task definition and those specified at run time). + * + * Not supported in Fargate. + */ + placementConstraints?: PlacementConstraint[]; + + /** + * What launch types this task definition should be compatible with. + */ + compatibility: Compatibility; + + /** + * The number of cpu units used by the task. + * Valid values, which determines your range of valid values for the memory parameter: + * 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB + * 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB + * 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB + * 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments + * 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments + */ + cpu?: string; + + /** + * The amount (in MiB) of memory used by the task. + * + * This field is required and you must use one of the following values, which determines your range of valid values + * for the cpu parameter: + * + * 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU) + * + * 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU) + * + * 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU) + * + * Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU) + * + * Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU) + */ + memoryMiB?: string; +} + +/** + * Base class for Ecs and Fargate task definitions + */ +export class TaskDefinition extends cdk.Construct { + /** + * The family name of this task definition + */ + public readonly family: string; + + /** + * ARN of this task definition + */ + public readonly taskDefinitionArn: string; + + /** + * Task role used by this task definition + */ + public readonly taskRole: iam.Role; + + /** + * Network mode used by this task definition + */ + public readonly networkMode: NetworkMode; + + /** + * Default container for this task + * + * Load balancers will send traffic to this container. The first + * essential container that is added to this task will become the default + * container. + */ + public defaultContainer?: ContainerDefinition; + + /** + * What launching modes this task is compatible with + */ + public compatibility: Compatibility; + + /** + * All containers + */ + protected readonly containers = new Array(); + + /** + * All volumes + */ + private readonly volumes: cloudformation.TaskDefinitionResource.VolumeProperty[] = []; + + /** + * Execution role for this task definition + * + * Will be created as needed. + */ + private executionRole?: iam.Role; + + /** + * Placement constraints for task instances + */ + private readonly placementConstraints = new Array(); + + constructor(parent: cdk.Construct, name: string, props: TaskDefinitionProps) { + super(parent, name); + + this.family = props.family || this.uniqueId; + this.compatibility = props.compatibility; + + if (props.volumes) { + props.volumes.forEach(v => this.addVolume(v)); + } + + this.networkMode = props.networkMode !== undefined ? props.networkMode : + isFargateCompatible(this.compatibility) ? NetworkMode.AwsVpc : NetworkMode.Bridge; + if (isFargateCompatible(this.compatibility) && this.networkMode !== NetworkMode.AwsVpc) { + throw new Error(`Fargate tasks can only have AwsVpc network mode, got: ${this.networkMode}`); + } + + if (props.placementConstraints && props.placementConstraints.length > 0 && isFargateCompatible(this.compatibility)) { + throw new Error('Cannot set placement constraints on tasks that run on Fargate'); + } + + if (isFargateCompatible(this.compatibility) && (!props.cpu || !props.memoryMiB)) { + throw new Error(`Fargate-compatible tasks require both CPU (${props.cpu}) and memory (${props.memoryMiB}) specifications`); + } + + this.executionRole = props.executionRole; + + this.taskRole = props.taskRole || new iam.Role(this, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + const taskDef = new cloudformation.TaskDefinitionResource(this, 'Resource', { + containerDefinitions: new cdk.Token(() => this.containers.map(x => x.renderContainerDefinition())), + volumes: new cdk.Token(() => this.volumes), + executionRoleArn: new cdk.Token(() => this.executionRole && this.executionRole.roleArn), + family: this.family, + taskRoleArn: this.taskRole.roleArn, + requiresCompatibilities: [ + ...(isEc2Compatible(props.compatibility) ? ["EC2"] : []), + ...(isFargateCompatible(props.compatibility) ? ["FARGATE"] : []), + ], + networkMode: this.networkMode, + placementConstraints: !isFargateCompatible(this.compatibility) ? new cdk.Token(this.placementConstraints) : undefined, + cpu: props.cpu, + memory: props.memoryMiB, + }); + + if (props.placementConstraints) { + props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); + } + + this.taskDefinitionArn = taskDef.taskDefinitionArn; + } + + /** + * Add a policy statement to the Task Role + */ + public addToTaskRolePolicy(statement: iam.PolicyStatement) { + this.taskRole.addToPolicy(statement); + } + + public addToExecutionRolePolicy(statement: iam.PolicyStatement) { + if (!this.executionRole) { + this.executionRole = new iam.Role(this, 'ExecutionRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + } + this.executionRole.addToPolicy(statement); + } + + /** + * Create a new container to this task definition + */ + public addContainer(id: string, props: ContainerDefinitionProps) { + const container = new ContainerDefinition(this, id, this, props); + this.containers.push(container); + if (this.defaultContainer === undefined && container.essential) { + this.defaultContainer = container; + } + + return container; + } + + /** + * Add a volume to this task definition + */ + public addVolume(volume: Volume) { + this.volumes.push(volume); + } + + /** + * Validate this task definition + */ + public validate(): string[] { + const ret = super.validate(); + + if (isEc2Compatible(this.compatibility)) { + // EC2 mode validations + + // Container sizes + for (const container of this.containers) { + if (!container.memoryLimitSpecified) { + ret.push(`ECS Container ${container.id} must have at least one of 'memoryLimitMiB' or 'memoryReservationMiB' specified`); + } + } + } + return ret; + } + + /** + * Constrain where tasks can be placed + */ + public addPlacementConstraint(constraint: PlacementConstraint) { + if (isFargateCompatible(this.compatibility)) { + throw new Error('Cannot set placement constraints on tasks that run on Fargate'); + } + const pc = this.renderPlacementConstraint(constraint); + this.placementConstraints.push(pc); + } + + /** + * Render the placement constraints + */ + private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { + return { + type: pc.type, + expression: pc.expression + }; + } +} + +/** + * The Docker networking mode to use for the containers in the task. + */ +export enum NetworkMode { + /** + * The task's containers do not have external connectivity and port mappings can't be specified in the container definition. + */ + None = 'none', + + /** + * The task utilizes Docker's built-in virtual network which runs inside each container instance. + */ + Bridge = 'bridge', + + /** + * The task is allocated an elastic network interface. + */ + AwsVpc = 'awsvpc', + + /** + * The task bypasses Docker's built-in virtual network and maps container ports directly to the EC2 instance's network interface directly. + * + * In this mode, you can't run multiple instantiations of the same task on a + * single container instance when port mappings are used. + */ + Host = 'host', +} + +/** + * Volume definition + */ +export interface Volume { + /** + * Path on the host + */ + host?: Host; + + /** + * A name for the volume + */ + name?: string; + // FIXME add dockerVolumeConfiguration +} + +/** + * A volume host + */ +export interface Host { + /** + * Source path on the host + */ + sourcePath?: string; +} + +/** + * A constraint on how instances should be placed + */ +export interface PlacementConstraint { + /** + * The type of constraint + */ + type: PlacementConstraintType; + + /** + * Additional information for the constraint + */ + expression?: string; +} + +/** + * A placement constraint type + */ +export enum PlacementConstraintType { + /** + * Place each task on a different instance + */ + DistinctInstance = "distinctInstance", + + /** + * Place tasks only on instances matching the expression in 'expression' + */ + MemberOf = "memberOf" +} + +/** + * Task compatibility + */ +export enum Compatibility { + /** + * Task should be launchable on EC2 clusters + */ + Ec2, + + /** + * Task should be launchable on Fargate clusters + */ + Fargate, + + /** + * Task should be launchable on both types of clusters + */ + Ec2AndFargate +} + +function isEc2Compatible(comp: Compatibility) { + return comp === Compatibility.Ec2 || comp === Compatibility.Ec2AndFargate; +} + +function isFargateCompatible(comp: Compatibility) { + return comp === Compatibility.Fargate || comp === Compatibility.Ec2AndFargate; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 6c42f2e40e338..a425042ccf998 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -1,6 +1,6 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { BaseTaskDefinition, NetworkMode } from './base/base-task-definition'; +import { NetworkMode, TaskDefinition } from './base/task-definition'; import { IContainerImage } from './container-image'; import { cloudformation } from './ecs.generated'; import { LinuxParameters } from './linux-parameters'; @@ -220,9 +220,9 @@ export class ContainerDefinition extends cdk.Construct { /** * The task definition this container definition is part of */ - private readonly taskDefinition: BaseTaskDefinition; + private readonly taskDefinition: TaskDefinition; - constructor(parent: cdk.Construct, id: string, taskDefinition: BaseTaskDefinition, private readonly props: ContainerDefinitionProps) { + constructor(parent: cdk.Construct, id: string, taskDefinition: TaskDefinition, private readonly props: ContainerDefinitionProps) { super(parent, id); this.essential = props.essential !== undefined ? props.essential : true; this.taskDefinition = taskDefinition; diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 8a35bcc954570..46760e45be59d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -3,7 +3,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import elb = require('@aws-cdk/aws-elasticloadbalancing'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; -import { BaseTaskDefinition, NetworkMode } from '../base/base-task-definition'; +import { NetworkMode } from '../base/base-task-definition'; import { cloudformation } from '../ecs.generated'; import { IEc2Cluster } from './ec2-cluster'; import { Ec2TaskDefinition } from './ec2-task-definition'; @@ -67,9 +67,6 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { */ public readonly clusterName: string; - protected readonly taskDef: BaseTaskDefinition; - - private readonly taskDefinition: Ec2TaskDefinition; private readonly constraints: cloudformation.ServiceResource.PlacementConstraintProperty[]; private readonly strategies: cloudformation.ServiceResource.PlacementStrategyProperty[]; private readonly daemon: boolean; @@ -86,7 +83,7 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { placementConstraints: new cdk.Token(() => this.constraints), placementStrategies: new cdk.Token(() => this.strategies), schedulingStrategy: props.daemon ? 'DAEMON' : 'REPLICA', - }, props.cluster.clusterName); + }, props.cluster.clusterName, props.taskDefinition); this.clusterName = props.cluster.clusterName; this.constraints = []; @@ -101,9 +98,6 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { this.connections.addSecurityGroup(...props.cluster.connections.securityGroups); } - this.taskDefinition = props.taskDefinition; - this.taskDef = props.taskDefinition; - if (props.placeOnDistinctInstances) { this.constraints.push({ type: 'distinctInstance' }); } diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts index a683727b9e1af..86d67a1728704 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts @@ -1,15 +1,16 @@ import cdk = require('@aws-cdk/cdk'); -import { BaseTaskDefinition, BaseTaskDefinitionProps, Compatibilities, NetworkMode } from '../base/base-task-definition'; -import { cloudformation } from '../ecs.generated'; +import { CommonTaskDefinitionProps, Compatibility, NetworkMode, PlacementConstraint, TaskDefinition } from '../base/task-definition'; /** * Properties to define an ECS task definition */ -export interface Ec2TaskDefinitionProps extends BaseTaskDefinitionProps { +export interface Ec2TaskDefinitionProps extends CommonTaskDefinitionProps { /** * The Docker networking mode to use for the containers in the task. * - * @default NetworkMode.Bridge + * On Fargate, the only supported networking mode is AwsVpc. + * + * @default NetworkMode.Bridge for EC2 tasks, AwsVpc for Fargate tasks. */ networkMode?: NetworkMode; @@ -26,91 +27,12 @@ export interface Ec2TaskDefinitionProps extends BaseTaskDefinitionProps { /** * Define Tasks to run on an ECS cluster */ -export class Ec2TaskDefinition extends BaseTaskDefinition { - /** - * The networkmode configuration of this task - */ - public readonly networkMode: NetworkMode; - - /** - * Placement constraints for task instances - */ - private readonly placementConstraints: cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty[]; - +export class Ec2TaskDefinition extends TaskDefinition { constructor(parent: cdk.Construct, name: string, props: Ec2TaskDefinitionProps = {}) { - const networkMode = props.networkMode || NetworkMode.Bridge; - - super(parent, name, props, { - networkMode, - requiresCompatibilities: [Compatibilities.Ec2], - placementConstraints: new cdk.Token(() => this.placementConstraints) + super(parent, name, { + ...props, + compatibility: Compatibility.Ec2, + placementConstraints: props.placementConstraints, }); - - this.networkMode = networkMode; - this.placementConstraints = []; - - if (props.placementConstraints) { - props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); - } } - - public validate(): string[] { - const ret = super.validate(); - - for (const container of this.containers) { - if (!container.memoryLimitSpecified) { - ret.push(`ECS Container ${container.id} must have at least one of 'memoryLimitMiB' or 'memoryReservationMiB' specified`); - } - } - - return ret; - } - - /** - * Constrain where tasks can be placed - */ - private addPlacementConstraint(constraint: PlacementConstraint) { - const pc = this.renderPlacementConstraint(constraint); - this.placementConstraints.push(pc); - } - - /** - * Render the placement constraints - */ - private renderPlacementConstraint(pc: PlacementConstraint): cloudformation.TaskDefinitionResource.TaskDefinitionPlacementConstraintProperty { - return { - type: pc.type, - expression: pc.expression - }; - } -} - -/** - * A constraint on how instances should be placed - */ -export interface PlacementConstraint { - /** - * The type of constraint - */ - type: PlacementConstraintType; - - /** - * Additional information for the constraint - */ - expression?: string; -} - -/** - * A placement constraint type - */ -export enum PlacementConstraintType { - /** - * Place each task on a different instance - */ - DistinctInstance = "distinctInstance", - - /** - * Place tasks only on instances matching the expression in 'expression' - */ - MemberOf = "memberOf" -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 1ecc32a7ce548..ea2c346898744 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -1,7 +1,6 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; -import { BaseTaskDefinition } from '../base/base-task-definition'; import { IFargateCluster } from './fargate-cluster'; import { FargateTaskDefinition } from './fargate-task-definition'; @@ -55,28 +54,19 @@ export interface FargateServiceProps extends BaseServiceProps { * Start a service on an ECS cluster */ export class FargateService extends BaseService { - /** - * The Task Definition for this service - */ - public readonly taskDefinition: FargateTaskDefinition; - protected readonly taskDef: BaseTaskDefinition; - constructor(parent: cdk.Construct, name: string, props: FargateServiceProps) { super(parent, name, props, { cluster: props.cluster.clusterName, taskDefinition: props.taskDefinition.taskDefinitionArn, launchType: 'FARGATE', platformVersion: props.platformVersion, - }, props.cluster.clusterName); + }, props.cluster.clusterName, props.taskDefinition); this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcPlacement, props.securityGroup); if (!props.taskDefinition.defaultContainer) { throw new Error('A TaskDefinition must have at least one essential container'); } - - this.taskDefinition = props.taskDefinition; - this.taskDef = props.taskDefinition; } } diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index 37d2eba0a4174..2c700e03ffd71 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -1,10 +1,10 @@ import cdk = require('@aws-cdk/cdk'); -import { BaseTaskDefinition, BaseTaskDefinitionProps, Compatibilities, NetworkMode } from '../base/base-task-definition'; +import { CommonTaskDefinitionProps, Compatibility, NetworkMode, TaskDefinition } from '../base/task-definition'; /** * Properties to define a Fargate Task */ -export interface FargateTaskDefinitionProps extends BaseTaskDefinitionProps { +export interface FargateTaskDefinitionProps extends CommonTaskDefinitionProps { /** * The number of cpu units used by the task. * Valid values, which determines your range of valid values for the memory parameter: @@ -42,18 +42,19 @@ export interface FargateTaskDefinitionProps extends BaseTaskDefinitionProps { /** * A definition for Tasks on a Fargate cluster */ -export class FargateTaskDefinition extends BaseTaskDefinition { +export class FargateTaskDefinition extends TaskDefinition { /** * The configured network mode */ public readonly networkMode = NetworkMode.AwsVpc; constructor(parent: cdk.Construct, name: string, props: FargateTaskDefinitionProps = {}) { - super(parent, name, props, { + super(parent, name, { + ...props, cpu: props.cpu || '256', - memory: props.memoryMiB || '512', + memoryMiB: props.memoryMiB || '512', + compatibility: Compatibility.Fargate, networkMode: NetworkMode.AwsVpc, - requiresCompatibilities: [Compatibilities.Fargate] }); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 0a16d2075453e..f98ac058f0f81 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,7 +1,7 @@ export * from './base/base-cluster'; export * from './base/base-service'; -export * from './base/base-task-definition'; export * from './base/scalable-task-count'; +export * from './base/task-definition'; export * from './container-definition'; export * from './container-image'; diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts index 346d53e8e3bdd..5b57fc043e16d 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts @@ -19,7 +19,7 @@ export = { PlacementConstraints: [], Volumes: [], NetworkMode: ecs.NetworkMode.Bridge, - RequiresCompatibilities: [ecs.Compatibilities.Ec2] + RequiresCompatibilities: ["EC2"] })); // test error if no container defs? diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts index daff091baa022..3c89f52d27c5d 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts @@ -16,7 +16,7 @@ export = { ContainerDefinitions: [], Volumes: [], NetworkMode: ecs.NetworkMode.AwsVpc, - RequiresCompatibilities: [ecs.Compatibilities.Fargate], + RequiresCompatibilities: ["FARGATE"], Cpu: "256", Memory: "512", })); diff --git a/packages/@aws-cdk/aws-ecs/test/test.task-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.task-definition.ts new file mode 100644 index 0000000000000..8bf044dd09ae4 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/test.task-definition.ts @@ -0,0 +1,25 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../lib'); + +export = { + "A task definition with both compatibilities defaults to networkmode AwsVpc"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.Ec2AndFargate, + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + NetworkMode: "awsvpc", + })); + + test.done(); + } +}; diff --git a/packages/aws-cdk/lib/api/toolkit-info.ts b/packages/aws-cdk/lib/api/toolkit-info.ts index b139b95095ff7..762fd1b1ff07a 100644 --- a/packages/aws-cdk/lib/api/toolkit-info.ts +++ b/packages/aws-cdk/lib/api/toolkit-info.ts @@ -34,6 +34,7 @@ export class ToolkitInfo { bucketEndpoint: string, environment: cxapi.Environment }) { + this.sdk = props.sdk; } public get bucketUrl() { diff --git a/scripts/build-typescript.sh b/scripts/build-typescript.sh index 965a3fe38d5df..98bc1dbd77538 100755 --- a/scripts/build-typescript.sh +++ b/scripts/build-typescript.sh @@ -39,4 +39,4 @@ cat < tsconfig.json "_generated_by_jsii_": "Generated by jsii - safe to delete, and ideally should be in .gitignore" } EOF -tsc -p . "$@" +node_modules/.bin/tsc -p . "$@" From ed0956a85ed77a1dd34bb14c8689ea1951246caf Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 5 Nov 2018 16:19:26 +0100 Subject: [PATCH 131/140] Fix import --- packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 46760e45be59d..f035a97fc8363 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -3,7 +3,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import elb = require('@aws-cdk/aws-elasticloadbalancing'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; -import { NetworkMode } from '../base/base-task-definition'; +import { NetworkMode } from '../base/task-definition'; import { cloudformation } from '../ecs.generated'; import { IEc2Cluster } from './ec2-cluster'; import { Ec2TaskDefinition } from './ec2-task-definition'; From 33a35b9928d94563b28f0899fb253578785fc0b0 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 5 Nov 2018 15:54:07 -0800 Subject: [PATCH 132/140] One EcsCluster to Rule Them All --- .../@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts | 4 +- .../{ec2/ec2-cluster.ts => ecs-cluster.ts} | 61 +++++++++---- .../aws-ecs/lib/fargate/fargate-cluster.ts | 87 ------------------- .../aws-ecs/lib/fargate/fargate-service.ts | 4 +- packages/@aws-cdk/aws-ecs/lib/index.ts | 3 +- .../aws-ecs/lib/load-balanced-ecs-service.ts | 4 +- .../load-balanced-fargate-service-applet.ts | 4 +- .../lib/load-balanced-fargate-service.ts | 4 +- .../aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts | 2 +- .../aws-ecs/test/ec2/integ.lb-bridge-nw.ts | 2 +- .../aws-ecs/test/ec2/test.ec2-service.ts | 30 +++---- .../aws-ecs/test/fargate/integ.asset-image.ts | 2 +- .../test/fargate/integ.lb-awsvpc-nw.ts | 2 +- .../test/fargate/test.fargate-cluster.ts | 41 --------- .../test/fargate/test.fargate-service.ts | 8 +- ...est.ec2-cluster.ts => test.ecs-cluster.ts} | 22 ++--- packages/@aws-cdk/aws-ecs/test/test.l3s.ts | 4 +- 17 files changed, 93 insertions(+), 191 deletions(-) rename packages/@aws-cdk/aws-ecs/lib/{ec2/ec2-cluster.ts => ecs-cluster.ts} (83%) delete mode 100644 packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts delete mode 100644 packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-cluster.ts rename packages/@aws-cdk/aws-ecs/test/{ec2/test.ec2-cluster.ts => test.ecs-cluster.ts} (87%) diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index f035a97fc8363..73949090efbab 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -4,8 +4,8 @@ import elb = require('@aws-cdk/aws-elasticloadbalancing'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; import { NetworkMode } from '../base/task-definition'; +import { IEcsCluster } from '../ecs-cluster'; import { cloudformation } from '../ecs.generated'; -import { IEc2Cluster } from './ec2-cluster'; import { Ec2TaskDefinition } from './ec2-task-definition'; /** @@ -15,7 +15,7 @@ export interface Ec2ServiceProps extends BaseServiceProps { /** * Cluster where service will be deployed */ - cluster: IEc2Cluster; + cluster: IEcsCluster; /** * Task Definition used for running tasks in the service diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/ecs-cluster.ts similarity index 83% rename from packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts rename to packages/@aws-cdk/aws-ecs/lib/ecs-cluster.ts index 86638fa95e63b..95966609d1204 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ecs-cluster.ts @@ -3,24 +3,34 @@ import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; +import { cloudformation } from './ecs.generated'; /** * Properties to define an ECS cluster */ -// tslint:disable-next-line:no-empty-interface -export interface Ec2ClusterProps extends BaseClusterProps { +export interface EcsClusterProps { + /** + * A name for the cluster. + * + * @default CloudFormation-generated name + */ + clusterName?: string; + + /** + * The VPC where your ECS instances will be running or your ENIs will be deployed + */ + vpc: ec2.VpcNetworkRef; } /** * A container cluster that runs on your EC2 instances */ -export class Ec2Cluster extends BaseCluster implements IEc2Cluster { +export class EcsCluster extends cdk.Construct implements IEcsCluster { /** * Import an existing cluster */ - public static import(parent: cdk.Construct, name: string, props: ImportedEc2ClusterProps): IEc2Cluster { - return new ImportedEc2Cluster(parent, name, props); + public static import(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps): IEcsCluster { + return new ImportedEcsCluster(parent, name, props); } /** @@ -28,8 +38,29 @@ export class Ec2Cluster extends BaseCluster implements IEc2Cluster { */ public readonly connections: ec2.Connections = new ec2.Connections(); - constructor(parent: cdk.Construct, name: string, props: Ec2ClusterProps) { - super(parent, name, props); + /** + * The VPC this cluster was created in. + */ + public readonly vpc: ec2.VpcNetworkRef; + + /** + * The ARN of this cluster + */ + public readonly clusterArn: string; + + /** + * The name of this cluster + */ + public readonly clusterName: string; + + constructor(parent: cdk.Construct, name: string, props: EcsClusterProps) { + super(parent, name); + + const cluster = new cloudformation.ClusterResource(this, 'Resource', {clusterName: props.clusterName}); + + this.vpc = props.vpc; + this.clusterArn = cluster.clusterArn; + this.clusterName = cluster.clusterName; } /** @@ -84,9 +115,9 @@ export class Ec2Cluster extends BaseCluster implements IEc2Cluster { } /** - * Export the Ec2Cluster + * Export the EcsCluster */ - public export(): ImportedEc2ClusterProps { + public export(): ImportedEcsClusterProps { return { clusterName: new cdk.Output(this, 'ClusterName', { value: this.clusterName }).makeImportValue().toString(), vpc: this.vpc.export(), @@ -149,7 +180,7 @@ export class EcsOptimizedAmi implements ec2.IMachineImageSource { /** * An ECS cluster */ -export interface IEc2Cluster { +export interface IEcsCluster { /** * Name of the cluster */ @@ -169,7 +200,7 @@ export interface IEc2Cluster { /** * Properties to import an ECS cluster */ -export interface ImportedEc2ClusterProps { +export interface ImportedEcsClusterProps { /** * Name of the cluster */ @@ -187,9 +218,9 @@ export interface ImportedEc2ClusterProps { } /** - * An Ec2Cluster that has been imported + * An EcsCluster that has been imported */ -class ImportedEc2Cluster extends cdk.Construct implements IEc2Cluster { +class ImportedEcsCluster extends cdk.Construct implements IEcsCluster { /** * Name of the cluster */ @@ -205,7 +236,7 @@ class ImportedEc2Cluster extends cdk.Construct implements IEc2Cluster { */ public readonly connections = new ec2.Connections(); - constructor(parent: cdk.Construct, name: string, props: ImportedEc2ClusterProps) { + constructor(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps) { super(parent, name); this.clusterName = props.clusterName; this.vpc = ec2.VpcNetworkRef.import(this, "vpc", props.vpc); diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts deleted file mode 100644 index a720237127eea..0000000000000 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-cluster.ts +++ /dev/null @@ -1,87 +0,0 @@ -import ec2 = require('@aws-cdk/aws-ec2'); -import cdk = require('@aws-cdk/cdk'); -import { BaseCluster, BaseClusterProps } from '../base/base-cluster'; - -/** - * Properties to define a Fargate cluster - */ -// tslint:disable-next-line:no-empty-interface -export interface FargateClusterProps extends BaseClusterProps { -} - -/** - * Define a cluster to run tasks on managed instances - */ -export class FargateCluster extends BaseCluster implements IFargateCluster { - /** - * Import an existing Fargate cluster - */ - public static import(parent: cdk.Construct, name: string, props: ImportedFargateClusterProps): IFargateCluster { - return new ImportedFargateCluster(parent, name, props); - } - - constructor(parent: cdk.Construct, name: string, props: FargateClusterProps) { - super(parent, name, props); - } - - /** - * Export the FargateCluster - */ - public export(): ImportedFargateClusterProps { - return { - clusterName: new cdk.Output(this, 'ClusterName', { value: this.clusterName }).makeImportValue().toString(), - vpc: this.vpc.export(), - }; - } -} - -/** - * A Fargate cluster - */ -export interface IFargateCluster { - /** - * Name of the cluster - */ - readonly clusterName: string; - - /** - * VPC where Task ENIs will be placed - */ - readonly vpc: ec2.VpcNetworkRef; -} - -/** - * Properties to import a Fargate cluster - */ -export interface ImportedFargateClusterProps { - /** - * Name of the cluster - */ - clusterName: string; - - /** - * VPC where Task ENIs should be placed - */ - vpc: ec2.VpcNetworkRefProps; -} - -/** - * A FargateCluster that has been imported - */ -class ImportedFargateCluster extends cdk.Construct implements IFargateCluster { - /** - * Name of the cluster - */ - public readonly clusterName: string; - - /** - * VPC where ENIs will be placed - */ - public readonly vpc: ec2.VpcNetworkRef; - - constructor(parent: cdk.Construct, name: string, props: ImportedFargateClusterProps) { - super(parent, name); - this.clusterName = props.clusterName; - this.vpc = ec2.VpcNetworkRef.import(this, "vpc", props.vpc); - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index ea2c346898744..2ac3034576e12 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -1,7 +1,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; -import { IFargateCluster } from './fargate-cluster'; +import { IEcsCluster } from '../ecs-cluster'; import { FargateTaskDefinition } from './fargate-task-definition'; /** @@ -11,7 +11,7 @@ export interface FargateServiceProps extends BaseServiceProps { /** * Cluster where service will be deployed */ - cluster: IFargateCluster; // should be required? do we assume 'default' exists? + cluster: IEcsCluster; // should be required? do we assume 'default' exists? /** * Task Definition used for running tasks in the service diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index f98ac058f0f81..1d1ce147d705f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -5,12 +5,11 @@ export * from './base/task-definition'; export * from './container-definition'; export * from './container-image'; +export * from './ecs-cluster'; -export * from './ec2/ec2-cluster'; export * from './ec2/ec2-service'; export * from './ec2/ec2-task-definition'; -export * from './fargate/fargate-cluster'; export * from './fargate/fargate-service'; export * from './fargate/fargate-task-definition'; diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts index 62487d7d00ec2..22264cdd92295 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts @@ -1,9 +1,9 @@ import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); import { IContainerImage } from './container-image'; -import { IEc2Cluster } from './ec2/ec2-cluster'; import { Ec2Service } from './ec2/ec2-service'; import { Ec2TaskDefinition } from './ec2/ec2-task-definition'; +import { IEcsCluster } from './ecs-cluster'; /** * Properties for a LoadBalancedEc2Service @@ -12,7 +12,7 @@ export interface LoadBalancedEc2ServiceProps { /** * The cluster where your Fargate service will be deployed */ - cluster: IEc2Cluster; + cluster: IEcsCluster; /** * The image to start. diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts index 274a664ff6ef4..8127794bb245d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts @@ -1,7 +1,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { DockerHub } from './container-image'; -import { FargateCluster } from './fargate/fargate-cluster'; +import { EcsCluster } from './ecs-cluster'; import { LoadBalancedFargateService } from './load-balanced-fargate-service'; /** @@ -80,7 +80,7 @@ export class LoadBalancedFargateServiceApplet extends cdk.Stack { super(parent, id, props); const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); - const cluster = new FargateCluster(this, 'Cluster', { vpc }); + const cluster = new EcsCluster(this, 'Cluster', { vpc }); // Instantiate Fargate Service with just cluster and image new LoadBalancedFargateService(this, "FargateService", { diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts index 474048ec8f7e6..028b8fd5913d4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts @@ -1,7 +1,7 @@ import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); import { IContainerImage } from './container-image'; -import { IFargateCluster } from './fargate/fargate-cluster'; +import { IEcsCluster } from './ecs-cluster'; import { FargateService } from './fargate/fargate-service'; import { FargateTaskDefinition } from './fargate/fargate-task-definition'; @@ -12,7 +12,7 @@ export interface LoadBalancedFargateServiceProps { /** * The cluster where your Fargate service will be deployed */ - cluster: IFargateCluster; + cluster: IEcsCluster; /** * The image to start diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts index c61b06a7c338d..de0f3419af31f 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts @@ -9,7 +9,7 @@ const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); +const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts index 8c00625228206..75bb468b9660d 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts @@ -9,7 +9,7 @@ const stack = new cdk.Stack(app, 'aws-ecs-integ-ecs'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); +const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts index f51f5d21fa361..d7677be0e0254 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts @@ -12,7 +12,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -31,7 +31,7 @@ export = { Ref: "Ec2TaskDef0226F28C" }, Cluster: { - Ref: "Ec2ClusterEE43E89D" + Ref: "EcsCluster97242B84" }, DeploymentConfiguration: { MaximumPercent: 200, @@ -52,7 +52,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); // THEN @@ -72,7 +72,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); // THEN @@ -90,7 +90,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -117,7 +117,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: NetworkMode.AwsVpc }); @@ -168,7 +168,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -196,7 +196,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -226,7 +226,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -256,7 +256,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -282,7 +282,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -311,7 +311,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -337,7 +337,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -367,7 +367,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -395,7 +395,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'VPC'); - const cluster = new ecs.Ec2Cluster(stack, 'Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TD', { networkMode: ecs.NetworkMode.Host }); const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image('test'), diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.ts index 4366cf2ae6722..99b9ca43d8808 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.ts @@ -7,7 +7,7 @@ const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -const cluster = new ecs.FargateCluster(stack, 'Cluster', { vpc }); +const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); Array.isArray(cluster); Array.isArray(path); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index b3c6f9401d46a..7921fa2a4f55f 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -8,7 +8,7 @@ const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -const cluster = new ecs.FargateCluster(stack, 'FargateCluster', { vpc }); +const cluster = new ecs.EcsCluster(stack, 'FargateCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { memoryMiB: '1GB', diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-cluster.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-cluster.ts deleted file mode 100644 index f25bcaf95985e..0000000000000 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-cluster.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert'; -import ec2 = require('@aws-cdk/aws-ec2'); -import cdk = require('@aws-cdk/cdk'); -import { Test } from 'nodeunit'; -import ecs = require('../../lib'); - -export = { - "When creating a Fargate Cluster": { - "with only required properties set, it correctly sets default properties"(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - new ecs.FargateCluster(stack, 'FargateCluster', { - vpc, - }); - - expect(stack).to(haveResource("AWS::ECS::Cluster")); - - expect(stack).to(haveResource("AWS::EC2::VPC", { - CidrBlock: '10.0.0.0/16', - EnableDnsHostnames: true, - EnableDnsSupport: true, - InstanceTenancy: ec2.DefaultInstanceTenancy.Default, - Tags: [ - { - Key: "Name", - Value: "MyVpc" - } - ] - })); - - expect(stack).notTo(haveResource("AWS::EC2::SecurityGroup")); - expect(stack).notTo(haveResource("AWS::AutoScaling::LaunchConfiguration")); - expect(stack).notTo(haveResource("AWS::AutoScaling::AutoScalingGroup")); - expect(stack).notTo(haveResource("AWS::IAM::Role")); - expect(stack).notTo(haveResource("AWS::IAM::Policy")); - - test.done(); - }, - } -}; diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts index 30132983d62a7..48a2d05ac29b2 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -10,7 +10,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.FargateCluster(stack, 'FargateCluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); taskDefinition.addContainer("web", { @@ -28,7 +28,7 @@ export = { Ref: "FargateTaskDefC6FB60B4" }, Cluster: { - Ref: "FargateCluster7CCD5F93" + Ref: "EcsCluster97242B84" }, DeploymentConfiguration: { MaximumPercent: 200, @@ -85,7 +85,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.FargateCluster(stack, 'FargateCluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); // THEN @@ -103,7 +103,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.FargateCluster(stack, 'FargateCluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); taskDefinition.addContainer("web", { diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts similarity index 87% rename from packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts rename to packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts index fab9632d2a261..6d5d716d2bc3e 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts @@ -3,7 +3,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import { InstanceType } from '@aws-cdk/aws-ec2'; import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; -import ecs = require('../../lib'); +import ecs = require('../lib'); export = { "When creating an ECS Cluster": { @@ -11,7 +11,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc, }); @@ -38,12 +38,12 @@ export = { ImageId: "", // Should this not be the latest image ID? InstanceType: "t2.micro", IamInstanceProfile: { - Ref: "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471" + Ref: "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3" }, SecurityGroups: [ { "Fn::GetAtt": [ - "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroup912E1231", "GroupId" ] } @@ -55,7 +55,7 @@ export = { [ "#!/bin/bash\necho ECS_CLUSTER=", { - Ref: "Ec2ClusterEE43E89D" + Ref: "EcsCluster97242B84" }, // tslint:disable-next-line:max-line-length " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" @@ -70,13 +70,13 @@ export = { MinSize: "0", DesiredCapacity: "1", LaunchConfigurationName: { - Ref: "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A" + Ref: "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1" }, Tags: [ { Key: "Name", PropagateAtLaunch: true, - Value: "Ec2Cluster/DefaultAutoScalingGroup" + Value: "EcsCluster/DefaultAutoScalingGroup" } ], VPCZoneIdentifier: [ @@ -93,7 +93,7 @@ export = { })); expect(stack).to(haveResource("AWS::EC2::SecurityGroup", { - GroupDescription: "Ec2Cluster/DefaultAutoScalingGroup/InstanceSecurityGroup", + GroupDescription: "EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup", SecurityGroupEgress: [ { CidrIp: "0.0.0.0/0", @@ -105,7 +105,7 @@ export = { Tags: [ { Key: "Name", - Value: "Ec2Cluster/DefaultAutoScalingGroup" + Value: "EcsCluster/DefaultAutoScalingGroup" } ], VpcId: { @@ -161,7 +161,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new InstanceType("m3.large") }); @@ -179,7 +179,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.Ec2Cluster(stack, 'Ec2Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro'), instanceCount: 3 diff --git a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts index 57998d842358d..87d2f0e3a8a22 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts @@ -9,7 +9,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'VPC'); - const cluster = new ecs.Ec2Cluster(stack, 'Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); // WHEN new ecs.LoadBalancedEc2Service(stack, 'Service', { @@ -28,7 +28,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'VPC'); - const cluster = new ecs.FargateCluster(stack, 'Cluster', { vpc }); + const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); // WHEN new ecs.LoadBalancedFargateService(stack, 'Service', { From 243190105be2abd8d6be9948654dca254278f519 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 5 Nov 2018 22:26:14 -0800 Subject: [PATCH 133/140] Update README --- packages/@aws-cdk/aws-ecs/README.md | 93 +++++++++++++++++++---------- 1 file changed, 62 insertions(+), 31 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index db30ed5344652..8bed2f86c9a63 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -5,7 +5,7 @@ Service** (ECS). The simplest example of using this library looks like this: ```ts // Create an ECS cluster -const cluster = new ecs.Ec2Cluster(this, 'Cluster', { +const cluster = new ecs.EcsCluster(this, 'Cluster', { vpc, }); @@ -28,9 +28,8 @@ const ecsService = new ecs.LoadBalancedEc2Service(this, 'Service', { There are two sets of constructs in this library; one to run tasks on ECS and one to run Tasks on Fargate. -- Use the `Ec2Cluster` and `Ec2Service` constructs to run tasks on EC2 - instances running in your account. -- Use the `FargateCluster` and `FargateService` constructs to run tasks on +- Use the `Ec2TaskDefinition` and `Ec2Service` constructs to run tasks on EC2 instances running in your account. +- Use the `FargateTaskDefinition` and `FargateService` constructs to run tasks on instances that are managed for you by AWS. Here are the main differences: @@ -52,29 +51,29 @@ For more information on EC2 vs Fargate and networking see the AWS Documentation: ### Clusters -An `Ec2Cluster` or `FargateCluster` defines the infrastructure to run your +An `EcsCluster` defines the infrastructure to run your tasks on. You can run many tasks on a single cluster. -To create a Fargate cluster, backed by machines managed by AWS, go: +To create a cluster that can run Fargate tasks, go: ```ts -const cluster = new ecs.FargateCluster(this, 'Cluster', { +const cluster = new ecs.EcsCluster(this, 'Cluster', { vpc: vpc }); ``` -If you create an ECS cluster you also have to create and add machines to it -to run the tasks scheduled on the cluster. Typically, you will add an -AutoScalingGroup with instances running the latest ECS-optimized AMI to -the cluster. There is a method to build and add such an AutoScalingGroup -automatically, or you can supply a customized AutoScalingGroup that -you construct yourself. It's possible to add multiple AutoScalingGroups +If you wish to use tasks with EC2 launch-type, you also have to add capacity to +your cluster in order for tasks to be scheduled on your instances. Typically, +you will add an AutoScalingGroup with instances running the latest +ECS-optimized AMI to the cluster. There is a method to build and add such an +AutoScalingGroup automatically, or you can supply a customized AutoScalingGroup +that you construct yourself. It's possible to add multiple AutoScalingGroups with various instance types if you want to. -Creating an EC2 cluster and adding capacity to it looks like this: +Creating an ECS cluster and adding capacity to it looks like this: ```ts -const cluster = new ecs.Ec2Cluster(this, 'Cluster', { +const cluster = new ecs.EcsCluster(this, 'Cluster', { vpc: vpc }); @@ -97,38 +96,71 @@ cluster.addAutoScalingGroupCapacity(autoScalingGroup); ``` ### Task definitions - -A `TaskDefinition` describes what a single copy of a **Task** should look like. +A Task Definition describes what a single copy of a **Task** should look like. A task definition has one or more containers; typically, it has one main container (the *default container* is the first one that's added to the task definition, and it will be marked *essential*) and optionally some supporting containers which are used to support the main container, doings things like upload logs or metrics to monitoring services. -When creating a Task Definition you have to specify what kind of -clusters you intend to run it on: EC2 clusters, Fargate clusters, or -both: +To run a task or service with EC2 launch type, use the `Ec2TaskDefinition`. For Fargate tasks/services, use the +`FargateTaskDefinition`. These classes provide a simplified API that only contain +properties relevant for that specific launch type. + +For a `FargateTaskDefinition`, specify the task size (`memoryMiB` and `cpu`): ```ts -const taskDefinition = new ecs.TaskDefinition(this, 'TaskDef', { +const fargateTaskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { memoryMiB: '512' cpu: 256, - compatibility: ecs.Compatibility.Ec2AndFargate, }); ``` - -To add containers to a `TaskDefinition`, call `addContainer()`: +To add containers to a Task Definition, call `addContainer()`: ```ts -taskDefinition.addContainer('main', { +const container = fargateTaskDefinition.addContainer(this, { // Use an image from DockerHub image: ecs.DockerHub.image('amazon/amazon-ecs-sample') + // ... other options here ... +}); +``` + +For a `Ec2TaskDefinition`: + +```ts +const ec2TaskDefinition = new ecs.Ec2TaskDefinition(this, 'TaskDef', { + networkMode: bridge +}); + +const container = ec2TaskDefinition.addContainer(this, { + // Use an image from DockerHub + image: ecs.DockerHub.image('amazon/amazon-ecs-sample'), + memoryLimitMiB: 1024 + // ... other options here ... +}); +``` + +You can specify container properties when you add them to the task definition, or with various methods, e.g.: + +```ts +container.addPortMappings({ + containerPort: 3000 +}) +``` + +If you wish to use a TaskDefinition that can be used with either EC2 or Fargate launch types, there is also the `TaskDefinition` construct. + +When creating a Task Definition you have to specify what kind of +tasks you intend to run: EC2, Fargate, or both: + +```ts +const taskDefinition = new ecs.TaskDefinition(this, 'TaskDef', { + memoryMiB: '512' + cpu: 256, + networkMode: 'awsvpc', + compatibility: ecs.Compatibility.Ec2AndFargate, }); ``` -If you're not trying to construct task definitions that can run on multiple -cluster types, you can directly instantiate `Ec2TaskDefinition` or -`FargateTaskDefinition`, classes with a simplified API that only contain -properties relevant for that specific launch type. #### Images @@ -205,5 +237,4 @@ EC2 instance group so that your instance count scales with demand. - [ ] Instance AutoScaling - [ ] Service Discovery Integration -- [ ] Private registry authentication - +- [ ] Private registry authentication \ No newline at end of file From 7c0a148b9e8af7afb0f1d5bb8dfbb575ac9adce2 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 5 Nov 2018 23:03:35 -0800 Subject: [PATCH 134/140] Update integ test expectations --- .../test/ec2/integ.lb-awsvpc-nw.expected.json | 40 ++++++++-------- .../test/ec2/integ.lb-bridge-nw.expected.json | 48 +++++++++---------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json index 577349b090e32..c655174233d6e 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json @@ -305,13 +305,13 @@ } } }, - "Ec2ClusterEE43E89D": { + "EcsCluster97242B84": { "Type": "AWS::ECS::Cluster" }, - "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E": { + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroup912E1231": { "Type": "AWS::EC2::SecurityGroup", "Properties": { - "GroupDescription": "aws-ecs-integ/Ec2Cluster/DefaultAutoScalingGroup/InstanceSecurityGroup", + "GroupDescription": "aws-ecs-integ/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", @@ -323,7 +323,7 @@ "Tags": [ { "Key": "Name", - "Value": "aws-ecs-integ/Ec2Cluster/DefaultAutoScalingGroup" + "Value": "aws-ecs-integ/EcsCluster/DefaultAutoScalingGroup" } ], "VpcId": { @@ -331,7 +331,7 @@ } } }, - "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898": { + "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -348,7 +348,7 @@ } } }, - "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD": { + "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -372,36 +372,36 @@ ], "Version": "2012-10-17" }, - "PolicyName": "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD", + "PolicyName": "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80", "Roles": [ { - "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863" } ] } }, - "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471": { + "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3": { "Type": "AWS::IAM::InstanceProfile", "Properties": { "Roles": [ { - "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863" } ] } }, - "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A": { + "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { "ImageId": "ami-1234", "InstanceType": "t2.micro", "IamInstanceProfile": { - "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471" + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3" }, "SecurityGroups": [ { "Fn::GetAtt": [ - "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroup912E1231", "GroupId" ] } @@ -413,7 +413,7 @@ [ "#!/bin/bash\necho ECS_CLUSTER=", { - "Ref": "Ec2ClusterEE43E89D" + "Ref": "EcsCluster97242B84" }, " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" ] @@ -422,24 +422,24 @@ } }, "DependsOn": [ - "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898", - "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD" + "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863", + "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80" ] }, - "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0": { + "EcsClusterDefaultAutoScalingGroupASGC1A785DB": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { "MaxSize": "1", "MinSize": "0", "DesiredCapacity": "1", "LaunchConfigurationName": { - "Ref": "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A" + "Ref": "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1" }, "Tags": [ { "Key": "Name", "PropagateAtLaunch": true, - "Value": "aws-ecs-integ/Ec2Cluster/DefaultAutoScalingGroup" + "Value": "aws-ecs-integ/EcsCluster/DefaultAutoScalingGroup" } ], "VPCZoneIdentifier": [ @@ -528,7 +528,7 @@ "Ref": "TaskDef54694570" }, "Cluster": { - "Ref": "Ec2ClusterEE43E89D" + "Ref": "EcsCluster97242B84" }, "DeploymentConfiguration": { "MaximumPercent": 200, diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json index 828607c0abf3d..7e26702951204 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json @@ -305,13 +305,13 @@ } } }, - "Ec2ClusterEE43E89D": { + "EcsCluster97242B84": { "Type": "AWS::ECS::Cluster" }, - "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E": { + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroup912E1231": { "Type": "AWS::EC2::SecurityGroup", "Properties": { - "GroupDescription": "aws-ecs-integ-ecs/Ec2Cluster/DefaultAutoScalingGroup/InstanceSecurityGroup", + "GroupDescription": "aws-ecs-integ-ecs/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", @@ -323,7 +323,7 @@ "Tags": [ { "Key": "Name", - "Value": "aws-ecs-integ-ecs/Ec2Cluster/DefaultAutoScalingGroup" + "Value": "aws-ecs-integ-ecs/EcsCluster/DefaultAutoScalingGroup" } ], "VpcId": { @@ -331,7 +331,7 @@ } } }, - "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroupfromawsecsintegecsLBSecurityGroup7DA90129808025D51C31": { + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroupfromawsecsintegecsLBSecurityGroup7DA9012980800B834EB8": { "Type": "AWS::EC2::SecurityGroupIngress", "Properties": { "IpProtocol": "tcp", @@ -339,7 +339,7 @@ "FromPort": 8080, "GroupId": { "Fn::GetAtt": [ - "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroup912E1231", "GroupId" ] }, @@ -352,7 +352,7 @@ "ToPort": 8080 } }, - "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898": { + "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -369,7 +369,7 @@ } } }, - "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD": { + "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -393,36 +393,36 @@ ], "Version": "2012-10-17" }, - "PolicyName": "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD", + "PolicyName": "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80", "Roles": [ { - "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863" } ] } }, - "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471": { + "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3": { "Type": "AWS::IAM::InstanceProfile", "Properties": { "Roles": [ { - "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863" } ] } }, - "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A": { + "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { "ImageId": "ami-1234", "InstanceType": "t2.micro", "IamInstanceProfile": { - "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471" + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3" }, "SecurityGroups": [ { "Fn::GetAtt": [ - "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroup912E1231", "GroupId" ] } @@ -434,7 +434,7 @@ [ "#!/bin/bash\necho ECS_CLUSTER=", { - "Ref": "Ec2ClusterEE43E89D" + "Ref": "EcsCluster97242B84" }, " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" ] @@ -443,24 +443,24 @@ } }, "DependsOn": [ - "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898", - "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD" + "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863", + "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80" ] }, - "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0": { + "EcsClusterDefaultAutoScalingGroupASGC1A785DB": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { "MaxSize": "1", "MinSize": "0", "DesiredCapacity": "1", "LaunchConfigurationName": { - "Ref": "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A" + "Ref": "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1" }, "Tags": [ { "Key": "Name", "PropagateAtLaunch": true, - "Value": "aws-ecs-integ-ecs/Ec2Cluster/DefaultAutoScalingGroup" + "Value": "aws-ecs-integ-ecs/EcsCluster/DefaultAutoScalingGroup" } ], "VPCZoneIdentifier": [ @@ -550,7 +550,7 @@ "Ref": "TaskDef54694570" }, "Cluster": { - "Ref": "Ec2ClusterEE43E89D" + "Ref": "EcsCluster97242B84" }, "DeploymentConfiguration": { "MaximumPercent": 200, @@ -618,7 +618,7 @@ } } }, - "LBSecurityGrouptoawsecsintegecsEc2ClusterDefaultAutoScalingGroupInstanceSecurityGroup03E2DA498080B03BF019": { + "LBSecurityGrouptoawsecsintegecsEcsClusterDefaultAutoScalingGroupInstanceSecurityGroupE3116410808033398DFA": { "Type": "AWS::EC2::SecurityGroupEgress", "Properties": { "GroupId": { @@ -631,7 +631,7 @@ "Description": "Load balancer to target", "DestinationSecurityGroupId": { "Fn::GetAtt": [ - "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroup912E1231", "GroupId" ] }, From 32db35d963dd4bc19b0f43142b40af64b27db4d8 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Mon, 5 Nov 2018 23:34:32 -0800 Subject: [PATCH 135/140] Add moar unit tests --- .../aws-ecs/test/ec2/test.ec2-service.ts | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts index d7677be0e0254..485fa462f3cad 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts @@ -112,6 +112,37 @@ export = { test.done(); }, + "with a TaskDefinition with Bridge network mode": { + "it errors if vpcPlacement is specified"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { + networkMode: NetworkMode.Bridge + }); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + // THEN + test.throws(() => { + new ecs.Ec2Service(stack, "Ec2Service", { + cluster, + taskDefinition, + vpcPlacement: { + subnetsToUse: ec2.SubnetType.Public + } + }); + }); + + // THEN + test.done(); + }, + }, + "with a TaskDefinition with AwsVpc network mode": { "it creates a security group for the service"(test: Test) { // GIVEN @@ -161,7 +192,33 @@ export = { })); test.done(); - } + }, + + "it allows vpcPlacement"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); + const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { + networkMode: NetworkMode.AwsVpc + }); + + taskDefinition.addContainer("web", { + image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + new ecs.Ec2Service(stack, "Ec2Service", { + cluster, + taskDefinition, + vpcPlacement: { + subnetsToUse: ec2.SubnetType.Public + } + }); + + // THEN + test.done(); + }, }, "with distinctInstance placement constraint"(test: Test) { From 233a376056735beeffb34a98f03d443067f08587 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 6 Nov 2018 10:57:03 +0100 Subject: [PATCH 136/140] Rename EcsCluster => Cluster, add slightly more handler tests --- .../hello-cdk-ecs-declarative/cdk.json | 2 +- .../fargate-service.yml | 11 ++- .../hello-cdk-ecs/index.ts | 2 +- packages/@aws-cdk/aws-ecs/README.md | 11 +-- .../@aws-cdk/aws-ecs/lib/base/base-cluster.ts | 63 --------------- .../aws-ecs/lib/base/task-definition.ts | 31 ++++++-- .../lib/{ecs-cluster.ts => cluster.ts} | 56 ++++++++++--- .../@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts | 27 +++++-- .../aws-ecs/lib/fargate/fargate-service.ts | 13 ++- packages/@aws-cdk/aws-ecs/lib/index.ts | 3 +- .../aws-ecs/lib/load-balanced-ecs-service.ts | 4 +- .../load-balanced-fargate-service-applet.ts | 4 +- .../lib/load-balanced-fargate-service.ts | 4 +- packages/@aws-cdk/aws-ecs/lib/util.ts | 9 +++ .../aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts | 2 +- .../aws-ecs/test/ec2/integ.lb-bridge-nw.ts | 2 +- .../aws-ecs/test/ec2/test.ec2-service.ts | 47 +++++++---- .../aws-ecs/test/fargate/integ.asset-image.ts | 2 +- .../test/fargate/integ.lb-awsvpc-nw.ts | 2 +- .../test/fargate/test.fargate-service.ts | 6 +- .../@aws-cdk/aws-ecs/test/test.asset-image.ts | 79 ++++++++++++++++++- .../@aws-cdk/aws-ecs/test/test.ecs-cluster.ts | 6 +- packages/@aws-cdk/aws-ecs/test/test.l3s.ts | 5 +- 23 files changed, 251 insertions(+), 140 deletions(-) delete mode 100644 packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts rename packages/@aws-cdk/aws-ecs/lib/{ecs-cluster.ts => cluster.ts} (85%) create mode 100644 packages/@aws-cdk/aws-ecs/lib/util.ts diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/cdk.json b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/cdk.json index 01cdce7b82735..e953d82380eba 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/cdk.json +++ b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/cdk.json @@ -1,3 +1,3 @@ { - "app": "../node_modules/.bin/cdk-applet-js fargate-service.yml" + "app": "fargate-service.yml" } diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml index b00e3c4b0eb4c..036197b367e6b 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml +++ b/examples/cdk-examples-typescript/hello-cdk-ecs-declarative/fargate-service.yml @@ -1,5 +1,8 @@ # applet is loaded from the local ./test-applet.js file -applet: @aws-cdk/aws-ecs:LoadBalancedFargateServiceApplet -image: 'amazon/amazon-ecs-sample' -cpu: "2048" -memoryMiB: "1024" +applets: + LoadBalancedFargateService: + type: @aws-cdk/aws-ecs:LoadBalancedFargateServiceApplet + properties: + image: 'amazon/amazon-ecs-sample' + cpu: "2048" + memoryMiB: "1024" diff --git a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts index c9e0a46a86002..9d3c4b0d7d5c8 100644 --- a/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-ecs/index.ts @@ -12,7 +12,7 @@ class BonjourECS extends cdk.Stack { // deploy, but VPC creation is slow so we'll only have to do that once // and can iterate quickly on consuming stacks. Not doing that for now. const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); - const cluster = new ecs.Ec2Cluster(this, 'Ec2Cluster', { vpc }); + const cluster = new ecs.Cluster(this, 'Ec2Cluster', { vpc }); cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new InstanceType("t2.xlarge"), instanceCount: 3, diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 8bed2f86c9a63..7279b20a086a3 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -5,7 +5,7 @@ Service** (ECS). The simplest example of using this library looks like this: ```ts // Create an ECS cluster -const cluster = new ecs.EcsCluster(this, 'Cluster', { +const cluster = new ecs.Cluster(this, 'Cluster', { vpc, }); @@ -51,13 +51,13 @@ For more information on EC2 vs Fargate and networking see the AWS Documentation: ### Clusters -An `EcsCluster` defines the infrastructure to run your +A `Cluster` defines the infrastructure to run your tasks on. You can run many tasks on a single cluster. To create a cluster that can run Fargate tasks, go: ```ts -const cluster = new ecs.EcsCluster(this, 'Cluster', { +const cluster = new ecs.Cluster(this, 'Cluster', { vpc: vpc }); ``` @@ -73,7 +73,7 @@ with various instance types if you want to. Creating an ECS cluster and adding capacity to it looks like this: ```ts -const cluster = new ecs.EcsCluster(this, 'Cluster', { +const cluster = new ecs.Cluster(this, 'Cluster', { vpc: vpc }); @@ -148,7 +148,8 @@ container.addPortMappings({ }) ``` -If you wish to use a TaskDefinition that can be used with either EC2 or Fargate launch types, there is also the `TaskDefinition` construct. +If you wish to use a TaskDefinition that can be used with either EC2 or +Fargate launch types, there is also the `TaskDefinition` construct. When creating a Task Definition you have to specify what kind of tasks you intend to run: EC2, Fargate, or both: diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts deleted file mode 100644 index 614b880980288..0000000000000 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-cluster.ts +++ /dev/null @@ -1,63 +0,0 @@ -import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); -import ec2 = require('@aws-cdk/aws-ec2'); -import cdk = require('@aws-cdk/cdk'); -import { cloudformation } from '../ecs.generated'; - -/** - * Basic cluster properties - */ -export interface BaseClusterProps { - /** - * A name for the cluster. - * - * @default CloudFormation-generated name - */ - clusterName?: string; - - /** - * The VPC where your ECS instances will be running or your ENIs will be deployed - */ - vpc: ec2.VpcNetworkRef; -} - -/** - * Base class for Ecs and Fargate clusters - */ -export abstract class BaseCluster extends cdk.Construct { - /** - * The VPC this cluster was created in. - */ - public readonly vpc: ec2.VpcNetworkRef; - - /** - * The ARN of this cluster - */ - public readonly clusterArn: string; - - /** - * The name of this cluster - */ - public readonly clusterName: string; - - constructor(parent: cdk.Construct, name: string, props: BaseClusterProps) { - super(parent, name); - - const cluster = new cloudformation.ClusterResource(this, 'Resource', {clusterName: props.clusterName}); - - this.vpc = props.vpc; - this.clusterArn = cluster.clusterArn; - this.clusterName = cluster.clusterName; - } - - /** - * Return the given named metric for this Cluster - */ - public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { - return new cloudwatch.Metric({ - namespace: 'AWS/ECS', - metricName, - dimensions: { ClusterName: this.clusterName }, - ...props - }); - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index 120598e00ef1e..e51a7fbfc57ff 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -2,6 +2,7 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { ContainerDefinition, ContainerDefinitionProps } from '../container-definition'; import { cloudformation } from '../ecs.generated'; +import { isEc2Compatible, isFargateCompatible } from '../util'; /** * Properties common to all Task definitions @@ -273,6 +274,16 @@ export class TaskDefinition extends cdk.Construct { this.placementConstraints.push(pc); } + /** + * Extend this TaskDefinition with the given extension + * + * Extension can be used to apply a packaged modification to + * a task definition. + */ + public addExtension(extension: ITaskDefinitionExtension) { + extension.extend(this); + } + /** * Render the placement constraints */ @@ -388,10 +399,18 @@ export enum Compatibility { Ec2AndFargate } -function isEc2Compatible(comp: Compatibility) { - return comp === Compatibility.Ec2 || comp === Compatibility.Ec2AndFargate; -} - -function isFargateCompatible(comp: Compatibility) { - return comp === Compatibility.Fargate || comp === Compatibility.Ec2AndFargate; +/** + * An extension for Task Definitions + * + * Classes can want to make changes to a TaskDefinition (such as + * adding helper containers) can implement this interface, and can + * then be "added" to a TaskDefinition like so: + * + * taskDefinition.addExtension(new MyExtension("some_parameter")); + */ +export interface ITaskDefinitionExtension { + /** + * Apply the extension to the given TaskDefinition + */ + extend(taskDefinition: TaskDefinition): void; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts similarity index 85% rename from packages/@aws-cdk/aws-ecs/lib/ecs-cluster.ts rename to packages/@aws-cdk/aws-ecs/lib/cluster.ts index 95966609d1204..8e2c8466e20f7 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -8,7 +8,7 @@ import { cloudformation } from './ecs.generated'; /** * Properties to define an ECS cluster */ -export interface EcsClusterProps { +export interface ClusterProps { /** * A name for the cluster. * @@ -25,12 +25,12 @@ export interface EcsClusterProps { /** * A container cluster that runs on your EC2 instances */ -export class EcsCluster extends cdk.Construct implements IEcsCluster { +export class Cluster extends cdk.Construct implements ICluster { /** * Import an existing cluster */ - public static import(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps): IEcsCluster { - return new ImportedEcsCluster(parent, name, props); + public static import(parent: cdk.Construct, name: string, props: ImportedClusterProps): ICluster { + return new ImportedCluster(parent, name, props); } /** @@ -53,7 +53,12 @@ export class EcsCluster extends cdk.Construct implements IEcsCluster { */ public readonly clusterName: string; - constructor(parent: cdk.Construct, name: string, props: EcsClusterProps) { + /** + * Whether the cluster has EC2 capacity associated with it + */ + private _hasEc2Capacity: boolean = false; + + constructor(parent: cdk.Construct, name: string, props: ClusterProps) { super(parent, name); const cluster = new cloudformation.ClusterResource(this, 'Resource', {clusterName: props.clusterName}); @@ -84,6 +89,7 @@ export class EcsCluster extends cdk.Construct implements IEcsCluster { * Add compute capacity to this ECS cluster in the form of an AutoScalingGroup */ public addAutoScalingGroupCapacity(autoScalingGroup: autoscaling.AutoScalingGroup, options: AddAutoScalingGroupCapacityOptions = {}) { + this._hasEc2Capacity = true; this.connections.connections.addSecurityGroup(...autoScalingGroup.connections.securityGroups); // Tie instances to cluster @@ -115,13 +121,21 @@ export class EcsCluster extends cdk.Construct implements IEcsCluster { } /** - * Export the EcsCluster + * Whether the cluster has EC2 capacity associated with it */ - public export(): ImportedEcsClusterProps { + public get hasEc2Capacity(): boolean { + return this._hasEc2Capacity; + } + + /** + * Export the Cluster + */ + public export(): ImportedClusterProps { return { clusterName: new cdk.Output(this, 'ClusterName', { value: this.clusterName }).makeImportValue().toString(), vpc: this.vpc.export(), securityGroups: this.connections.securityGroups.map(sg => sg.export()), + hasEc2Capacity: this.hasEc2Capacity, }; } @@ -180,7 +194,7 @@ export class EcsOptimizedAmi implements ec2.IMachineImageSource { /** * An ECS cluster */ -export interface IEcsCluster { +export interface ICluster { /** * Name of the cluster */ @@ -195,12 +209,17 @@ export interface IEcsCluster { * Connections manager of the cluster instances */ readonly connections: ec2.Connections; + + /** + * Whether the cluster has EC2 capacity associated with it + */ + readonly hasEc2Capacity: boolean; } /** * Properties to import an ECS cluster */ -export interface ImportedEcsClusterProps { +export interface ImportedClusterProps { /** * Name of the cluster */ @@ -215,12 +234,19 @@ export interface ImportedEcsClusterProps { * Security group of the cluster instances */ securityGroups: ec2.SecurityGroupRefProps[]; + + /** + * Whether the given cluster has EC2 capacity + * + * @default true + */ + hasEc2Capacity?: boolean; } /** - * An EcsCluster that has been imported + * An Cluster that has been imported */ -class ImportedEcsCluster extends cdk.Construct implements IEcsCluster { +class ImportedCluster extends cdk.Construct implements ICluster { /** * Name of the cluster */ @@ -236,10 +262,16 @@ class ImportedEcsCluster extends cdk.Construct implements IEcsCluster { */ public readonly connections = new ec2.Connections(); - constructor(parent: cdk.Construct, name: string, props: ImportedEcsClusterProps) { + /** + * Whether the cluster has EC2 capacity + */ + public readonly hasEc2Capacity: boolean; + + constructor(parent: cdk.Construct, name: string, props: ImportedClusterProps) { super(parent, name); this.clusterName = props.clusterName; this.vpc = ec2.VpcNetworkRef.import(this, "vpc", props.vpc); + this.hasEc2Capacity = props.hasEc2Capacity !== false; let i = 1; for (const sgProps of props.securityGroups) { diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 73949090efbab..8625e5ac6ff91 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -3,10 +3,10 @@ import ec2 = require('@aws-cdk/aws-ec2'); import elb = require('@aws-cdk/aws-elasticloadbalancing'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; -import { NetworkMode } from '../base/task-definition'; -import { IEcsCluster } from '../ecs-cluster'; +import { NetworkMode, TaskDefinition } from '../base/task-definition'; +import { ICluster } from '../cluster'; import { cloudformation } from '../ecs.generated'; -import { Ec2TaskDefinition } from './ec2-task-definition'; +import { isEc2Compatible } from '../util'; /** * Properties to define an ECS service @@ -15,12 +15,12 @@ export interface Ec2ServiceProps extends BaseServiceProps { /** * Cluster where service will be deployed */ - cluster: IEcsCluster; + cluster: ICluster; /** * Task Definition used for running tasks in the service */ - taskDefinition: Ec2TaskDefinition; + taskDefinition: TaskDefinition; /** * In what subnets to place the task's ENIs @@ -70,12 +70,17 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { private readonly constraints: cloudformation.ServiceResource.PlacementConstraintProperty[]; private readonly strategies: cloudformation.ServiceResource.PlacementStrategyProperty[]; private readonly daemon: boolean; + private readonly cluster: ICluster; constructor(parent: cdk.Construct, name: string, props: Ec2ServiceProps) { if (props.daemon && props.desiredCount !== undefined) { throw new Error('Daemon mode launches one task on every instance. Don\'t supply desiredCount.'); } + if (!isEc2Compatible(props.taskDefinition.compatibility)) { + throw new Error('Supplied TaskDefinition is not configured for compatibility with EC2'); + } + super(parent, name, props, { cluster: props.cluster.clusterName, taskDefinition: props.taskDefinition.taskDefinitionArn, @@ -85,6 +90,7 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { schedulingStrategy: props.daemon ? 'DAEMON' : 'REPLICA', }, props.cluster.clusterName, props.taskDefinition); + this.cluster = props.cluster; this.clusterName = props.cluster.clusterName; this.constraints = []; this.strategies = []; @@ -216,6 +222,17 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { public metricCpuUtilization(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { return this.metric('CPUUtilization', props); } + + /** + * Validate this Ec2Service + */ + public validate(): string[] { + const ret = super.validate(); + if (!this.cluster.hasEc2Capacity) { + ret.push('Cluster for this service needs Ec2 capacity. Call addXxxCapacity() on the cluster.'); + } + return ret; + } } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 2ac3034576e12..44e451de48914 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -1,8 +1,9 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { BaseService, BaseServiceProps } from '../base/base-service'; -import { IEcsCluster } from '../ecs-cluster'; -import { FargateTaskDefinition } from './fargate-task-definition'; +import { TaskDefinition } from '../base/task-definition'; +import { ICluster } from '../cluster'; +import { isFargateCompatible } from '../util'; /** * Properties to define a Fargate service @@ -11,12 +12,12 @@ export interface FargateServiceProps extends BaseServiceProps { /** * Cluster where service will be deployed */ - cluster: IEcsCluster; // should be required? do we assume 'default' exists? + cluster: ICluster; // should be required? do we assume 'default' exists? /** * Task Definition used for running tasks in the service */ - taskDefinition: FargateTaskDefinition; + taskDefinition: TaskDefinition; /** * Assign public IP addresses to each task @@ -55,6 +56,10 @@ export interface FargateServiceProps extends BaseServiceProps { */ export class FargateService extends BaseService { constructor(parent: cdk.Construct, name: string, props: FargateServiceProps) { + if (!isFargateCompatible(props.taskDefinition.compatibility)) { + throw new Error('Supplied TaskDefinition is not configured for compatibility with Fargate'); + } + super(parent, name, props, { cluster: props.cluster.clusterName, taskDefinition: props.taskDefinition.taskDefinitionArn, diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 1d1ce147d705f..dc8da1b5027db 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -1,11 +1,10 @@ -export * from './base/base-cluster'; export * from './base/base-service'; export * from './base/scalable-task-count'; export * from './base/task-definition'; export * from './container-definition'; export * from './container-image'; -export * from './ecs-cluster'; +export * from './cluster'; export * from './ec2/ec2-service'; export * from './ec2/ec2-task-definition'; diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts index 22264cdd92295..6979ecacb8bfd 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-ecs-service.ts @@ -1,9 +1,9 @@ import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); +import { ICluster } from './cluster'; import { IContainerImage } from './container-image'; import { Ec2Service } from './ec2/ec2-service'; import { Ec2TaskDefinition } from './ec2/ec2-task-definition'; -import { IEcsCluster } from './ecs-cluster'; /** * Properties for a LoadBalancedEc2Service @@ -12,7 +12,7 @@ export interface LoadBalancedEc2ServiceProps { /** * The cluster where your Fargate service will be deployed */ - cluster: IEcsCluster; + cluster: ICluster; /** * The image to start. diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts index 8127794bb245d..e4abe93395551 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts @@ -1,7 +1,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); +import { Cluster } from './cluster'; import { DockerHub } from './container-image'; -import { EcsCluster } from './ecs-cluster'; import { LoadBalancedFargateService } from './load-balanced-fargate-service'; /** @@ -80,7 +80,7 @@ export class LoadBalancedFargateServiceApplet extends cdk.Stack { super(parent, id, props); const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); - const cluster = new EcsCluster(this, 'Cluster', { vpc }); + const cluster = new Cluster(this, 'Cluster', { vpc }); // Instantiate Fargate Service with just cluster and image new LoadBalancedFargateService(this, "FargateService", { diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts index 028b8fd5913d4..2287296468ea4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service.ts @@ -1,7 +1,7 @@ import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import cdk = require('@aws-cdk/cdk'); +import { ICluster } from './cluster'; import { IContainerImage } from './container-image'; -import { IEcsCluster } from './ecs-cluster'; import { FargateService } from './fargate/fargate-service'; import { FargateTaskDefinition } from './fargate/fargate-task-definition'; @@ -12,7 +12,7 @@ export interface LoadBalancedFargateServiceProps { /** * The cluster where your Fargate service will be deployed */ - cluster: IEcsCluster; + cluster: ICluster; /** * The image to start diff --git a/packages/@aws-cdk/aws-ecs/lib/util.ts b/packages/@aws-cdk/aws-ecs/lib/util.ts new file mode 100644 index 0000000000000..e12251822e338 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/util.ts @@ -0,0 +1,9 @@ +import { Compatibility } from "./base/task-definition"; + +export function isEc2Compatible(comp: Compatibility) { + return comp === Compatibility.Ec2 || comp === Compatibility.Ec2AndFargate; +} + +export function isFargateCompatible(comp: Compatibility) { + return comp === Compatibility.Fargate || comp === Compatibility.Ec2AndFargate; +} diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts index de0f3419af31f..b40be9b57e319 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts @@ -9,7 +9,7 @@ const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts index 75bb468b9660d..2d8cf306f3886 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts @@ -9,7 +9,7 @@ const stack = new cdk.Stack(app, 'aws-ecs-integ-ecs'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts index 485fa462f3cad..93a0fa4dc4b7f 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts @@ -12,7 +12,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -52,7 +53,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); // THEN @@ -72,7 +74,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); // THEN @@ -90,7 +92,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -117,7 +120,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: NetworkMode.Bridge }); @@ -148,7 +152,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: NetworkMode.AwsVpc }); @@ -198,7 +203,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: NetworkMode.AwsVpc }); @@ -225,7 +231,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -253,7 +260,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -283,7 +291,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -313,7 +322,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -339,7 +349,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -368,7 +379,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -394,7 +406,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -424,7 +437,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -452,7 +466,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'VPC'); - const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TD', { networkMode: ecs.NetworkMode.Host }); const container = taskDefinition.addContainer('web', { image: ecs.DockerHub.image('test'), diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.ts index 99b9ca43d8808..c80af87c3776a 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.ts @@ -7,7 +7,7 @@ const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); +const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); Array.isArray(cluster); Array.isArray(path); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index 7921fa2a4f55f..dd0d9343455b7 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -8,7 +8,7 @@ const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); -const cluster = new ecs.EcsCluster(stack, 'FargateCluster', { vpc }); +const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { memoryMiB: '1GB', diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts index 48a2d05ac29b2..42c2a83ac7482 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -10,7 +10,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); taskDefinition.addContainer("web", { @@ -85,7 +85,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); // THEN @@ -103,7 +103,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); taskDefinition.addContainer("web", { diff --git a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts index ef105dbca525f..a039803fc197b 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts @@ -30,9 +30,12 @@ export = { test.done(); }, - async 'exercise handler'(test: Test) { + async 'exercise handler create'(test: Test) { const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler'), { - 'aws-sdk': { '@noCallThru': true, ECR } + 'aws-sdk': { + '@noCallThru': true, + "ECR": ECRWithEmptyPolicy, + } }); let output; @@ -60,16 +63,86 @@ export = { test.done(); }, + + async 'exercise handler delete'(test: Test) { + const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler'), { + 'aws-sdk': { '@noCallThru': true, "ECR": ECRWithOwningPolicy } + }); + + let output; + async function response(responseStatus: string, reason: string, physId: string, data: any) { + output = { responseStatus, reason, physId, data }; + } + + await handler.handler({ + StackId: 'StackId', + ResourceProperties: { + RepositoryArn: 'RepositoryArn', + }, + RequestType: 'Delete', + ResponseURL: 'https://localhost/test' + }, { + logStreamName: 'xyz', + }, undefined, response); + + test.deepEqual(output, { + responseStatus: 'SUCCESS', + reason: 'OK', + physId: '', + data: { RepositoryUri: 'undefined.dkr.ecr.undefined.amazonaws.com/' } + }); + + test.done(); + }, }; +function ECRWithEmptyPolicy() { + return new ECR({ asdf: 'asdf' }); +} + +function ECRWithOwningPolicy() { + return new ECR({ + Statement: [ + { + Sid: 'StackId', + Effect: "Deny", + Action: "OwnedBy:CDKStack", + Principal: "*" + } + ] + }); +} + class ECR { + public constructor(private policy: any) { + } + public getRepositoryPolicy() { + const self = this; return { async promise() { return { - policyText: '{"asdf": "asdf"}' + policyText: JSON.stringify(self.policy) }; } }; } public setRepositoryPolicy() { return { async promise() { return; } }; } + + public listImages() { + return { async promise() { + return { imageIds: [] }; + } }; + } + + public batchDeleteImage() { + return { async promise() { + return {}; + } }; + } + + public deleteRepository() { + return { async promise() { + return {}; + } }; + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts index 6d5d716d2bc3e..be055197775cc 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts @@ -11,7 +11,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc, }); @@ -161,7 +161,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new InstanceType("m3.large") }); @@ -179,7 +179,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); - const cluster = new ecs.EcsCluster(stack, 'EcsCluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro'), instanceCount: 3 diff --git a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts index 87d2f0e3a8a22..900116a34fdb9 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts @@ -9,7 +9,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'VPC'); - const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); // WHEN new ecs.LoadBalancedEc2Service(stack, 'Service', { @@ -28,7 +29,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'VPC'); - const cluster = new ecs.EcsCluster(stack, 'Cluster', { vpc }); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); // WHEN new ecs.LoadBalancedFargateService(stack, 'Service', { From b6162481fafbbe9a63e84000bf55c3c25222b83b Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 6 Nov 2018 11:28:15 +0100 Subject: [PATCH 137/140] Make a single static accessor for all types of Container images --- .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 37 ++----- packages/@aws-cdk/aws-ecr/package.json | 1 - packages/@aws-cdk/aws-ecs/README.md | 12 ++- .../aws-ecs/lib/adopt-repository/handler.js | 102 ------------------ .../aws-ecs/lib/base/task-definition.ts | 22 ++-- .../aws-ecs/lib/container-definition.ts | 8 +- .../@aws-cdk/aws-ecs/lib/container-image.ts | 32 +++--- .../aws-ecs/lib/{ => images}/asset-image.ts | 24 ++--- .../@aws-cdk/aws-ecs/lib/images/dockerhub.ts | 26 +++++ packages/@aws-cdk/aws-ecs/lib/images/ecr.ts | 20 ++++ packages/@aws-cdk/aws-ecs/lib/index.ts | 5 +- .../load-balanced-fargate-service-applet.ts | 2 +- packages/@aws-cdk/aws-ecs/package.json | 1 + .../@aws-cdk/aws-ecs/test/test.asset-image.ts | 4 +- 14 files changed, 118 insertions(+), 178 deletions(-) delete mode 100644 packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js rename packages/@aws-cdk/aws-ecs/lib/{ => images}/asset-image.ts (83%) create mode 100644 packages/@aws-cdk/aws-ecs/lib/images/dockerhub.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/images/ecr.ts diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index db048ed40a44f..2e41764d145c0 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -1,4 +1,3 @@ -import ecs = require('@aws-cdk/aws-ecs'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); @@ -46,13 +45,6 @@ export abstract class RepositoryRef extends cdk.Construct { return `${parts.account}.dkr.ecr.${parts.region}.amazonaws.com/${parts.resourceName}`; } - /** - * Refer to a particular image tag from this repository - */ - public getImage(tag: string = "latest"): ecs.IContainerImage { - return new EcrImage(this, tag); - } - /** * Grant the given principal identity permissions to perform the actions on this repository */ @@ -70,6 +62,12 @@ export abstract class RepositoryRef extends cdk.Construct { */ public grantUseImage(identity?: iam.IPrincipal) { this.grant(identity, "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage"); + + if (identity) { + identity.addToPolicy(new iam.PolicyStatement() + .addActions("ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents") + .addAllResources()); + } } } @@ -93,25 +91,4 @@ class ImportedRepository extends RepositoryRef { public addToResourcePolicy(_statement: iam.PolicyStatement) { // FIXME: Add annotation about policy we dropped on the floor } -} - -class EcrImage implements ecs.IContainerImage { - public readonly imageName: string; - private readonly repositoryArn: string; - - constructor(repository: RepositoryRef, tag: string) { - this.imageName = `${repository.repositoryUri}:${tag}`; - this.repositoryArn = repository.repositoryArn; - } - - public bind(containerDefinition: ecs.ContainerDefinition): void { - // This image will be in ECR, so we need appropriate permissions. - containerDefinition.addToExecutionPolicy(new iam.PolicyStatement() - .addActions("ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage") - .addResource(this.repositoryArn)); - - containerDefinition.addToExecutionPolicy(new iam.PolicyStatement() - .addActions("ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents") - .addAllResources()); - } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index 81c0b1b89b1a6..82eb02f07efd2 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -60,7 +60,6 @@ }, "dependencies": { "@aws-cdk/aws-iam": "^0.14.1", - "@aws-cdk/aws-ecs": "^0.14.1", "@aws-cdk/cdk": "^0.14.1" }, "homepage": "https://github.com/awslabs/aws-cdk" diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 7279b20a086a3..5781b1575cecb 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -19,7 +19,7 @@ cluster.addDefaultAutoScalingGroupCapacity({ const ecsService = new ecs.LoadBalancedEc2Service(this, 'Service', { cluster, memoryLimitMiB: 512, - image: ecs.DockerHub.image("amazon/amazon-ecs-sample"), + image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"), }); ``` @@ -120,7 +120,7 @@ To add containers to a Task Definition, call `addContainer()`: ```ts const container = fargateTaskDefinition.addContainer(this, { // Use an image from DockerHub - image: ecs.DockerHub.image('amazon/amazon-ecs-sample') + image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"), // ... other options here ... }); ``` @@ -134,7 +134,7 @@ const ec2TaskDefinition = new ecs.Ec2TaskDefinition(this, 'TaskDef', { const container = ec2TaskDefinition.addContainer(this, { // Use an image from DockerHub - image: ecs.DockerHub.image('amazon/amazon-ecs-sample'), + image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"), memoryLimitMiB: 1024 // ... other options here ... }); @@ -168,10 +168,12 @@ const taskDefinition = new ecs.TaskDefinition(this, 'TaskDef', { Images supply the software that runs inside the container. Images can be obtained from either DockerHub or from ECR repositories: -* `ecs.DockerHub.image(imageName)`: use a publicly available image from +* `ecs.ContainerImage.fromDockerHub(imageName)`: use a publicly available image from DockerHub. -* `repository.getImage(tag)`: use the given ECR repository as the image +* `ecs.ContaienrImage.fromEcrRepository(repo, tag)`: use the given ECR repository as the image to start. +* `ecs.ContainerImage.fromAsset({ directory: './image' })`: build and upload an + image directly from a `Dockerfile` in your source directory. ### Service diff --git a/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js b/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js deleted file mode 100644 index 640b5d4e538f3..0000000000000 --- a/packages/@aws-cdk/aws-ecs/lib/adopt-repository/handler.js +++ /dev/null @@ -1,102 +0,0 @@ -const AWS = require('aws-sdk'); -const ecr = new AWS.ECR(); - -exports.handler = async function(event, context, _callback, respond) { - respond = respond || respondCFN; - try { - console.log(JSON.stringify(event)); - - const markerStatement = { - Sid: event.StackId, - Effect: "Deny", - Action: "OwnedBy:CDKStack", - Principal: "*" - }; - - function repoName(props) { - return props.RepositoryArn.split('/').slice(1).join('/'); - } - - // The repository must already exist - async function getAdopter(name) { - try { - const policyResponse = await ecr.getRepositoryPolicy({ repositoryName: name }).promise(); - const policy = JSON.parse(policyResponse.policyText); - // Search the policy for an adopter marker - return (policy.Statement || []).find((x) => x.Action === markerStatement.Action) || {}; - } catch (e) { - if (e.code !== 'RepositoryPolicyNotFoundException') { throw e; } - return {}; - } - } - - const repo = repoName(event.ResourceProperties); - const adopter = await getAdopter(repo); - if (event.RequestType === 'Delete') { - if (adopter.Sid !== markerStatement.Sid) { - throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); - } - try { - console.log('Deleting', repo); - const ids = (await ecr.listImages({ repositoryName: repo }).promise()).imageIds; - await ecr.batchDeleteImage({ repositoryName: repo, imageIds: ids }).promise(); - await ecr.deleteRepository({ repositoryName: repo }).promise(); - } catch(e) { - if (e.code !== 'RepositoryNotFoundException') { throw e; } - } - } - - if (event.RequestType === 'Create' || event.RequestType === 'Update') { - if (adopter.Sid !== undefined && adopter.Sid !== markerStatement.Sid) { - throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); - } - console.log('Adopting', repo); - await ecr.setRepositoryPolicy({ repositoryName: repo, policyText: JSON.stringify({ - Version: '2008-10-17', - Statement: [markerStatement] - }) }).promise(); - } - - const arn = event.ResourceProperties.RepositoryArn.split(':'); - await respond("SUCCESS", "OK", repo, { - RepositoryUri: `${arn[4]}.dkr.ecr.${arn[3]}.amazonaws.com/${repoName(event.ResourceProperties)}` - }); - } catch (e) { - console.log(e); - await respond("FAILED", e.message, context.logStreamName, {}); - } - - function respondCFN(responseStatus, reason, physId, data) { - const responseBody = JSON.stringify({ - Status: responseStatus, - Reason: reason, - PhysicalResourceId: physId, - StackId: event.StackId, - RequestId: event.RequestId, - LogicalResourceId: event.LogicalResourceId, - NoEcho: false, - Data: data - }); - - console.log('Responding', JSON.stringify(responseBody)); - - const parsedUrl = require('url').parse(event.ResponseURL); - const requestOptions = { - hostname: parsedUrl.hostname, - path: parsedUrl.path, - method: "PUT", - headers: { "content-type": "", "content-length": responseBody.length } - }; - - return new Promise((resolve, reject) => { - try { - const request = require('https').request(requestOptions, resolve); - request.on("error", reject); - request.write(responseBody); - request.end(); - } catch (e) { - reject(e); - } - }); - } -} diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index e51a7fbfc57ff..e0e2e0e9227d2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -215,13 +215,11 @@ export class TaskDefinition extends cdk.Construct { this.taskRole.addToPolicy(statement); } + /** + * Add a policy statement to the Execution Role + */ public addToExecutionRolePolicy(statement: iam.PolicyStatement) { - if (!this.executionRole) { - this.executionRole = new iam.Role(this, 'ExecutionRole', { - assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), - }); - } - this.executionRole.addToPolicy(statement); + this.obtainExecutionRole().addToPolicy(statement); } /** @@ -284,6 +282,18 @@ export class TaskDefinition extends cdk.Construct { extension.extend(this); } + /** + * Create the execution role if it doesn't exist + */ + public obtainExecutionRole(): iam.IRole { + if (!this.executionRole) { + this.executionRole = new iam.Role(this, 'ExecutionRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + } + return this.executionRole; + } + /** * Render the placement constraints */ diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index a425042ccf998..5b03605ff5c97 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -213,14 +213,14 @@ export class ContainerDefinition extends cdk.Construct { public readonly memoryLimitSpecified: boolean; /** - * The configured container links + * The task definition this container definition is part of */ - private readonly links = new Array(); + public readonly taskDefinition: TaskDefinition; /** - * The task definition this container definition is part of + * The configured container links */ - private readonly taskDefinition: TaskDefinition; + private readonly links = new Array(); constructor(parent: cdk.Construct, id: string, taskDefinition: TaskDefinition, private readonly props: ContainerDefinitionProps) { super(parent, id); diff --git a/packages/@aws-cdk/aws-ecs/lib/container-image.ts b/packages/@aws-cdk/aws-ecs/lib/container-image.ts index ee7f6b2440767..a441d3285318e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-image.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-image.ts @@ -1,4 +1,10 @@ +import ecr = require('@aws-cdk/aws-ecr'); +import cdk = require('@aws-cdk/cdk'); + import { ContainerDefinition } from './container-definition'; +import { AssetImage, AssetImageProps } from './images/asset-image'; +import { DockerHubImage } from './images/dockerhub'; +import { EcrImage } from './images/ecr'; /** * A container image @@ -16,25 +22,27 @@ export interface IContainerImage { } /** - * Factory for DockerHub images + * Constructs for types of container images */ -export class DockerHub { +export class ContainerImage { /** * Reference an image on DockerHub */ - public static image(name: string): IContainerImage { + public static fromDockerHub(name: string) { return new DockerHubImage(name); } -} -/** - * A DockerHub image - */ -class DockerHubImage implements IContainerImage { - constructor(public readonly imageName: string) { + /** + * Reference an image in an ECR repository + */ + public static fromEcrRepository(repository: ecr.RepositoryRef, tag: string = 'latest') { + return new EcrImage(repository, tag); } - public bind(_containerDefinition: ContainerDefinition): void { - // Nothing to do + /** + * Reference an image that's constructed directly from sources on disk + */ + public static fromAsset(parent: cdk.Construct, id: string, props: AssetImageProps) { + return new AssetImage(parent, id, props); } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/asset-image.ts b/packages/@aws-cdk/aws-ecs/lib/images/asset-image.ts similarity index 83% rename from packages/@aws-cdk/aws-ecs/lib/asset-image.ts rename to packages/@aws-cdk/aws-ecs/lib/images/asset-image.ts index 1e773e4000e36..ac0c0c63cbe7b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/lib/images/asset-image.ts @@ -1,12 +1,13 @@ import cfn = require('@aws-cdk/aws-cloudformation'); +import ecr = require('@aws-cdk/aws-ecr'); import iam = require('@aws-cdk/aws-iam'); import lambda = require('@aws-cdk/aws-lambda'); import cdk = require('@aws-cdk/cdk'); import cxapi = require('@aws-cdk/cx-api'); import fs = require('fs'); import path = require('path'); -import { ContainerDefinition } from './container-definition'; -import { IContainerImage } from './container-image'; +import { ContainerDefinition } from '../container-definition'; +import { IContainerImage } from '../container-image'; export interface AssetImageProps { /** @@ -30,9 +31,9 @@ export class AssetImage extends cdk.Construct implements IContainerImage { private readonly directory: string; /** - * ARN of the repository + * Repository where the image is stored */ - private readonly repositoryArn: string; + private repository: ecr.RepositoryRef; constructor(parent: cdk.Construct, id: string, props: AssetImageProps) { super(parent, id); @@ -66,7 +67,9 @@ export class AssetImage extends cdk.Construct implements IContainerImage { this.addMetadata(cxapi.ASSET_METADATA, asset); - this.repositoryArn = repositoryParameter.value.toString(); + this.repository = ecr.Repository.import(this, 'RepositoryObject', { + repositoryArn: repositoryParameter.value.toString(), + }); // Require that repository adoption happens first, so we route the // input ARN into the Custom Resource and then get the URI which we use to @@ -74,19 +77,12 @@ export class AssetImage extends cdk.Construct implements IContainerImage { // // If adoption fails (because the repository might be twice-adopted), we // haven't already started using the image. - const adopted = new AdoptRepository(this, 'AdoptRepository', { repositoryArn: this.repositoryArn }); + const adopted = new AdoptRepository(this, 'AdoptRepository', { repositoryArn: this.repository.repositoryArn }); this.imageName = `${adopted.repositoryUri}:${tagParameter.value}`; } public bind(containerDefinition: ContainerDefinition): void { - // This image will be in ECR, so we need appropriate permissions. - containerDefinition.addToExecutionPolicy(new iam.PolicyStatement() - .addActions("ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage") - .addResource(this.repositoryArn)); - - containerDefinition.addToExecutionPolicy(new iam.PolicyStatement() - .addActions("ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents") - .addAllResources()); + this.repository.grantUseImage(containerDefinition.taskDefinition.obtainExecutionRole()); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/images/dockerhub.ts b/packages/@aws-cdk/aws-ecs/lib/images/dockerhub.ts new file mode 100644 index 0000000000000..d0bb48bd357a1 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/images/dockerhub.ts @@ -0,0 +1,26 @@ +import { ContainerDefinition } from "../container-definition"; +import { IContainerImage } from "../container-image"; + +/** + * Factory for DockerHub images + */ +export class DockerHub { + /** + * Reference an image on DockerHub + */ + public static image(name: string): IContainerImage { + return new DockerHubImage(name); + } +} + +/** + * A DockerHub image + */ +export class DockerHubImage implements IContainerImage { + constructor(public readonly imageName: string) { + } + + public bind(_containerDefinition: ContainerDefinition): void { + // Nothing to do + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts b/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts new file mode 100644 index 0000000000000..f0c7803789120 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts @@ -0,0 +1,20 @@ +import ecr = require('@aws-cdk/aws-ecr'); +import { ContainerDefinition } from '../container-definition'; +import { IContainerImage } from '../container-image'; + +/** + * An image from an ECR repository + */ +export class EcrImage implements IContainerImage { + public readonly imageName: string; + private readonly repository: ecr.RepositoryRef; + + constructor(repository: ecr.RepositoryRef, tag: string) { + this.imageName = `${repository.repositoryUri}:${tag}`; + this.repository = repository; + } + + public bind(containerDefinition: ContainerDefinition): void { + this.repository.grantUseImage(containerDefinition.taskDefinition.obtainExecutionRole()); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index dc8da1b5027db..902efcdc04529 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -17,7 +17,10 @@ export * from './load-balanced-ecs-service'; export * from './load-balanced-fargate-service'; export * from './load-balanced-ecs-service'; export * from './load-balanced-fargate-service-applet'; -export * from './asset-image'; + +export * from './images/asset-image'; +export * from './images/dockerhub'; +export * from './images/ecr'; export * from './log-drivers/aws-log-driver'; export * from './log-drivers/log-driver'; diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts index e4abe93395551..1e3d00bb983de 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts @@ -1,7 +1,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { Cluster } from './cluster'; -import { DockerHub } from './container-image'; +import { DockerHub } from './images/dockerhub'; import { LoadBalancedFargateService } from './load-balanced-fargate-service'; /** diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 788eb15dd14c7..c3fd5fda3c240 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -66,6 +66,7 @@ "@aws-cdk/aws-cloudformation": "^0.14.1", "@aws-cdk/aws-cloudwatch": "^0.14.1", "@aws-cdk/aws-ec2": "^0.14.1", + "@aws-cdk/aws-ecr": "^0.14.1", "@aws-cdk/aws-elasticloadbalancing": "^0.14.1", "@aws-cdk/aws-elasticloadbalancingv2": "^0.14.1", "@aws-cdk/aws-iam": "^0.14.1", diff --git a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts index a039803fc197b..7e386ddcae252 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.asset-image.ts @@ -31,7 +31,7 @@ export = { }, async 'exercise handler create'(test: Test) { - const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler'), { + const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'images', 'adopt-repository', 'handler'), { 'aws-sdk': { '@noCallThru': true, "ECR": ECRWithEmptyPolicy, @@ -65,7 +65,7 @@ export = { }, async 'exercise handler delete'(test: Test) { - const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'adopt-repository', 'handler'), { + const handler = proxyquire(path.resolve(__dirname, '..', 'lib', 'images', 'adopt-repository', 'handler'), { 'aws-sdk': { '@noCallThru': true, "ECR": ECRWithOwningPolicy } }); From ea6eeae31c90770614fad5a0d27bbcea18d219f5 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 6 Nov 2018 12:53:46 +0100 Subject: [PATCH 138/140] Small fixes --- examples/cdk-examples-typescript/hello-cdk-fargate/index.ts | 4 ++-- packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts index 2d545c1b3cb9a..7df99aec374c3 100644 --- a/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts +++ b/examples/cdk-examples-typescript/hello-cdk-fargate/index.ts @@ -9,7 +9,7 @@ class BonjourFargate extends cdk.Stack { // Create VPC and Fargate Cluster // NOTE: Limit AZs to avoid reaching resource quotas const vpc = new ec2.VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); - const cluster = new ecs.FargateCluster(this, 'Cluster', { vpc }); + const cluster = new ecs.Cluster(this, 'Cluster', { vpc }); // Instantiate Fargate Service with just cluster and image const fargateService = new ecs.LoadBalancedFargateService(this, "FargateService", { @@ -26,4 +26,4 @@ const app = new cdk.App(); new BonjourFargate(app, 'Bonjour'); -app.run(); \ No newline at end of file +app.run(); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index e0e2e0e9227d2..c90edf81d98a4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -412,7 +412,7 @@ export enum Compatibility { /** * An extension for Task Definitions * - * Classes can want to make changes to a TaskDefinition (such as + * Classes that want to make changes to a TaskDefinition (such as * adding helper containers) can implement this interface, and can * then be "added" to a TaskDefinition like so: * From 98990b45937e4817ed0a8645214ec7493c4ca986 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 6 Nov 2018 13:48:21 +0100 Subject: [PATCH 139/140] Re-add missing handler --- packages/@aws-cdk/aws-ecs/.gitignore | 2 +- .../lib/images/adopt-registry/handler.js | 102 ++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/images/adopt-registry/handler.js diff --git a/packages/@aws-cdk/aws-ecs/.gitignore b/packages/@aws-cdk/aws-ecs/.gitignore index 9859788601252..b06f502af5be4 100644 --- a/packages/@aws-cdk/aws-ecs/.gitignore +++ b/packages/@aws-cdk/aws-ecs/.gitignore @@ -13,5 +13,5 @@ dist coverage .nycrc .LAST_PACKAGE -!lib/adopt-repository/* +!lib/images/adopt-registry/* *.snk diff --git a/packages/@aws-cdk/aws-ecs/lib/images/adopt-registry/handler.js b/packages/@aws-cdk/aws-ecs/lib/images/adopt-registry/handler.js new file mode 100644 index 0000000000000..640b5d4e538f3 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/images/adopt-registry/handler.js @@ -0,0 +1,102 @@ +const AWS = require('aws-sdk'); +const ecr = new AWS.ECR(); + +exports.handler = async function(event, context, _callback, respond) { + respond = respond || respondCFN; + try { + console.log(JSON.stringify(event)); + + const markerStatement = { + Sid: event.StackId, + Effect: "Deny", + Action: "OwnedBy:CDKStack", + Principal: "*" + }; + + function repoName(props) { + return props.RepositoryArn.split('/').slice(1).join('/'); + } + + // The repository must already exist + async function getAdopter(name) { + try { + const policyResponse = await ecr.getRepositoryPolicy({ repositoryName: name }).promise(); + const policy = JSON.parse(policyResponse.policyText); + // Search the policy for an adopter marker + return (policy.Statement || []).find((x) => x.Action === markerStatement.Action) || {}; + } catch (e) { + if (e.code !== 'RepositoryPolicyNotFoundException') { throw e; } + return {}; + } + } + + const repo = repoName(event.ResourceProperties); + const adopter = await getAdopter(repo); + if (event.RequestType === 'Delete') { + if (adopter.Sid !== markerStatement.Sid) { + throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); + } + try { + console.log('Deleting', repo); + const ids = (await ecr.listImages({ repositoryName: repo }).promise()).imageIds; + await ecr.batchDeleteImage({ repositoryName: repo, imageIds: ids }).promise(); + await ecr.deleteRepository({ repositoryName: repo }).promise(); + } catch(e) { + if (e.code !== 'RepositoryNotFoundException') { throw e; } + } + } + + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + if (adopter.Sid !== undefined && adopter.Sid !== markerStatement.Sid) { + throw new Error(`This repository is already owned by another stack: ${adopter.Sid}`); + } + console.log('Adopting', repo); + await ecr.setRepositoryPolicy({ repositoryName: repo, policyText: JSON.stringify({ + Version: '2008-10-17', + Statement: [markerStatement] + }) }).promise(); + } + + const arn = event.ResourceProperties.RepositoryArn.split(':'); + await respond("SUCCESS", "OK", repo, { + RepositoryUri: `${arn[4]}.dkr.ecr.${arn[3]}.amazonaws.com/${repoName(event.ResourceProperties)}` + }); + } catch (e) { + console.log(e); + await respond("FAILED", e.message, context.logStreamName, {}); + } + + function respondCFN(responseStatus, reason, physId, data) { + const responseBody = JSON.stringify({ + Status: responseStatus, + Reason: reason, + PhysicalResourceId: physId, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + NoEcho: false, + Data: data + }); + + console.log('Responding', JSON.stringify(responseBody)); + + const parsedUrl = require('url').parse(event.ResponseURL); + const requestOptions = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: "PUT", + headers: { "content-type": "", "content-length": responseBody.length } + }; + + return new Promise((resolve, reject) => { + try { + const request = require('https').request(requestOptions, resolve); + request.on("error", reject); + request.write(responseBody); + request.end(); + } catch (e) { + reject(e); + } + }); + } +} From 908db1f9f902d4ec65123500df7ae11dce493bfe Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 6 Nov 2018 14:18:41 +0100 Subject: [PATCH 140/140] Oops wrong name --- packages/@aws-cdk/aws-ecs/.gitignore | 2 +- .../lib/images/{adopt-registry => adopt-repository}/handler.js | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/@aws-cdk/aws-ecs/lib/images/{adopt-registry => adopt-repository}/handler.js (100%) diff --git a/packages/@aws-cdk/aws-ecs/.gitignore b/packages/@aws-cdk/aws-ecs/.gitignore index b06f502af5be4..ead522a97b70f 100644 --- a/packages/@aws-cdk/aws-ecs/.gitignore +++ b/packages/@aws-cdk/aws-ecs/.gitignore @@ -13,5 +13,5 @@ dist coverage .nycrc .LAST_PACKAGE -!lib/images/adopt-registry/* +!lib/images/adopt-repository/* *.snk diff --git a/packages/@aws-cdk/aws-ecs/lib/images/adopt-registry/handler.js b/packages/@aws-cdk/aws-ecs/lib/images/adopt-repository/handler.js similarity index 100% rename from packages/@aws-cdk/aws-ecs/lib/images/adopt-registry/handler.js rename to packages/@aws-cdk/aws-ecs/lib/images/adopt-repository/handler.js