├── .gitignore ├── README.md ├── index.js ├── lib ├── repos │ ├── ecr.js │ ├── none.js │ └── quay.js └── service.js ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS ECS Deployer Utility 2 | 3 | This tool helps simplify automated docker deployments to Amazon's ECS. In short the tool will: 4 | 5 | 1. Perform a number of pre-checks that all resources are ready to be released. 6 | 1. Register a new task definition revision with ECS. 7 | 1. Update one or more services with the new revision. 8 | 1. Scale your auto-scaling group up, so that the new revision can be deployed. This is optional. 9 | 1. Wait for ECS to complete the deploy. 10 | 1. Scale your auto-scaling group back down to normal size. This is optional. 11 | 12 | ## Getting Started 13 | 14 | ``` 15 | yarn add ecs-deployer --dev 16 | ``` 17 | 18 | ## Configuration 19 | 20 | Under the hood we are using the NodeJS AWS SDK. You can configure your credentials [several ways](http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/node-configuring.html). 21 | 22 | The AWS Region must be configured via an environment variable. Make sure to set `AWS_REGION` to whatever AWS region your resources are located in. 23 | 24 | The user should have the following policy attached: 25 | ``` 26 | { 27 | "Version": "2012-10-17", 28 | "Statement": [ 29 | { 30 | "Effect": "Allow", 31 | "Action": [ 32 | "ecs:DescribeClusters", 33 | "ecs:DescribeServices", 34 | "ecs:DescribeTaskDefinition", 35 | "ecs:DescribeTasks", 36 | "ecs:ListServices", 37 | "ecs:ListTaskDefinitions", 38 | "ecs:ListTasks", 39 | "ecs:RegisterTaskDefinition", 40 | "ecs:UpdateService", 41 | "autoscaling:DescribeAutoScalingGroups", 42 | "autoscaling:SetDesiredCapacity", 43 | "ecr:DescribeImages" 44 | ], 45 | "Resource": [ 46 | "*" 47 | ] 48 | } 49 | ] 50 | } 51 | ``` 52 | 53 | The `ecr:DescribeImages` is only needed if you are also hosting your images in ECR. 54 | 55 | ## Usage 56 | 57 | A sample deploy: 58 | ``` 59 | const EcsDeployer = require('ecs-deployer') 60 | 61 | const deployer = new EcsDeployer({ 62 | docker: { 63 | type: 'quay', // supported: 'quay', 'ecr', or 'none' (checking bypassed) 64 | url: 'https://quay.io/username/image-name', // required for quay 65 | auth: '' // required for quay 66 | repository: 'foo/bar' // required for ECR 67 | }, 68 | 69 | services: [ 70 | { 71 | taskDefinition: { 72 | "family": "foo" // This should already exist in ECS. Required. 73 | }, 74 | 75 | name: 'my-web-service', // ECS service name. Required. 76 | cluster: 'web', // ECS cluster name. Required. 77 | autoScaling: { 78 | name: 'my-web-autoscaling-group' // Optional 79 | }, 80 | imagePath: 'site/organization/repo' // Optional - example: quay.io/MyOrganization/my-repo 81 | } 82 | ] 83 | }); 84 | 85 | // Call deploy and give a version to deploy 86 | deployer.deploy('1.0.0').then(function() { 87 | console.log('Successfully deployed') 88 | }, function(err) { 89 | console.error('Failed to deploy'); 90 | console.error(err) 91 | }); 92 | 93 | // Optionally subscribe to progress events. 94 | deployer.on('progress', function(e) { 95 | console.log(e.service.name, e.msg); 96 | }) 97 | ``` 98 | 99 | ## Current limitations 100 | 101 | * Assumes a single region deployment. 102 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'), 2 | Promise = require('promise'), 3 | Service = require('./lib/service'), 4 | EventEmitter = require('events').EventEmitter, 5 | util = require('util') 6 | 7 | if (!AWS.config.region) { 8 | AWS.config.update({region: process.env.AWS_REGION}); 9 | } 10 | 11 | // imagePath can be optionally defined in the manifest to control a deploy 12 | var Deployer = function(app) { 13 | EventEmitter.call(this); 14 | this.app = app; 15 | 16 | this.services = []; 17 | var service; 18 | for (var i = 0; i < app.services.length; i++) { 19 | this.services.push(new Service(app.services[i], this)); 20 | } 21 | } 22 | 23 | util.inherits(Deployer, EventEmitter); 24 | 25 | /** 26 | * Determine if the app deployment is valid. 27 | */ 28 | Deployer.prototype._isValid = function() { 29 | var d = this; 30 | return new Promise(function(resolve, reject) { 31 | if (!d.app) { 32 | reject(new Error('No application object defined for deployment')); 33 | return; 34 | } 35 | 36 | if (!d.services) { 37 | reject(new Error('No services defined for deployment')); 38 | return; 39 | } 40 | 41 | if (!d.app.docker) { 42 | reject(new Error('No docker repo defined for deployment')); 43 | return; 44 | } 45 | 46 | resolve(); 47 | }) 48 | }; 49 | 50 | Deployer.prototype.isReady = function(version) { 51 | var deployer = this; 52 | 53 | var repoType = this.app.docker.type || 'quay'; 54 | var Repo = require(`./lib/repos/${repoType.toLowerCase()}`); 55 | var repo = new Repo(this.app.docker); 56 | 57 | var tagCheckDeferred = repo.isExists(version); 58 | tagCheckDeferred.then(() => { 59 | this.emit('progress', { msg: 'Found tagged docker image', service: { name: repoType }}); 60 | }) 61 | 62 | // TODO: Optionally check git too 63 | 64 | var deferreds = [ tagCheckDeferred ]; 65 | for (var i = 0; i < this.services.length; i++) { 66 | deferreds.push(this.services[i].isReady(version)); 67 | } 68 | 69 | return Promise.all(deferreds); 70 | }; 71 | 72 | // imagePath can be carried through the function calls as 'undefined' 73 | Deployer.prototype.deploy = function(version) { 74 | var d = this; 75 | 76 | return this._isValid().then(function() { 77 | return d.isReady(version).then(function() { 78 | d.emit('ready') 79 | 80 | var deferreds = []; 81 | for (var i = 0; i < d.services.length; i++) { 82 | deferreds.push(d.services[i].deploy(version, d.services[i].service.imagePath)); 83 | } 84 | 85 | // Number of services that have completed their deploys 86 | var completed = 0; 87 | 88 | return Promise.all(deferreds).then(function(completedServices) { 89 | for (var i = 0; i < completedServices.length; i++) { 90 | d.emit('deployed', completedServices[i]); 91 | } 92 | 93 | completed += completedServices.length; 94 | 95 | if (completed >= d.app.services.length) { 96 | d.emit('end'); 97 | } 98 | }, function(err) { 99 | d.emit('failure', err); 100 | 101 | completed += err.length; 102 | if (completed >= d.app.services.length) { 103 | d.emit('end'); 104 | } 105 | }) 106 | }); 107 | }); 108 | } 109 | 110 | Deployer.Service = Service; 111 | 112 | module.exports = Deployer 113 | -------------------------------------------------------------------------------- /lib/repos/ecr.js: -------------------------------------------------------------------------------- 1 | const Promise = require('promise'), 2 | AWS = require('aws-sdk'); 3 | 4 | const Ecr = function(config) { 5 | this.repository = config.repository; 6 | } 7 | 8 | const ecr = new AWS.ECR(); 9 | 10 | Ecr.prototype.isExists = function(tag) { 11 | return new Promise((resolve, reject) => { 12 | const params = { 13 | repositoryName: this.repository, 14 | imageIds: [ 15 | { 16 | imageTag: tag 17 | } 18 | ] 19 | }; 20 | 21 | ecr.describeImages(params, (err, data) => { 22 | if (err) { 23 | if (err.code === 'ImageNotFoundException') { 24 | const e = new Error('Unable to find tagged image in ECR'); 25 | e.response = err; 26 | return reject(e); 27 | } 28 | 29 | return reject(err); 30 | } 31 | 32 | resolve(true); 33 | }); 34 | }); 35 | } 36 | 37 | module.exports = Ecr; 38 | -------------------------------------------------------------------------------- /lib/repos/none.js: -------------------------------------------------------------------------------- 1 | var Promise = require('promise'), 2 | request = require('request'); 3 | 4 | // Dummy service, which does not perform any checks 5 | // Can also be used as a skeleton for new checks 6 | var None = function(config) { 7 | } 8 | 9 | None.prototype.isExists = function(tag) { 10 | var quay = this; 11 | return new Promise(function(resolve, reject) { 12 | return resolve(true); 13 | }); 14 | } 15 | 16 | module.exports = None; 17 | -------------------------------------------------------------------------------- /lib/repos/quay.js: -------------------------------------------------------------------------------- 1 | const Promise = require('promise'), 2 | request = require('request'); 3 | 4 | const Quay = function(config) { 5 | this.url = config.url; 6 | this.auth = config.auth; 7 | } 8 | 9 | Quay.prototype.isExists = function(tag) { 10 | return new Promise((resolve, reject) => { 11 | const query = { 12 | url: `${this.url}/tag/${tag}/images`, 13 | headers: { 14 | 'Authorization': `Bearer ${this.auth}` 15 | } 16 | }; 17 | 18 | request.get(query, (err, resp, body) => { 19 | if (err) return reject(err); 20 | 21 | if (resp.statusCode == 401) return reject(new Error('authentication failure')); 22 | 23 | if (resp.statusCode === 200) return resolve(true); 24 | const e = new Error('Unable to find tagged image in quay'); 25 | e.response = resp; 26 | reject(e); 27 | }); 28 | }); 29 | } 30 | 31 | module.exports = Quay; 32 | -------------------------------------------------------------------------------- /lib/service.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'), 2 | Promise = require('promise'); 3 | 4 | /** How long to wait when checking if the service has been deployed. Millis */ 5 | var DEPLOY_WAIT_INTERVAL = 15000; 6 | 7 | /** 8 | * @scope private 9 | * Utility method for parsing a URL into the key/object parts needed for S3. 10 | * 11 | * @param [String] url An S3 URL to deconstruct. 12 | * @return [Object] Object containing properties bucket and key. 13 | */ 14 | var _parseS3Url = function(url) { 15 | url = url.replace(/s3:\/\//i, '') 16 | var urlParts = url.split('/'); 17 | return { bucket: urlParts.shift(), key: urlParts.join('/') }; 18 | } 19 | 20 | /** 21 | * @scope private 22 | * Update an image string with the given version number. 23 | * 24 | * @param [String] image A docker image string. Usually looks like 'repo/image:tag' 25 | * @param [String] version The version number to update the image to. 26 | * @return [String] A new image string with the right version. 27 | */ 28 | var _updateImageVersion = function(image, version) { 29 | var imageParts = image.split(':'); 30 | return [imageParts[0], version].join(':'); 31 | } 32 | 33 | /** 34 | * @scope private 35 | * Polls for the deployment to be complete until it is. Deploy is complete when 36 | * all desired instances are running and all previous version instances have stopped. 37 | * 38 | * @param [String] taskArn The AWS ARN to the task we want deployed. 39 | * @param [Service] service The service object we are deploying for. 40 | * @param [Function] resolve The success callback. 41 | * @param [Function] reject The error callback. 42 | * @param [Integer] retries The number of times we should retry polling. 43 | */ 44 | var _pollDeployment = function(taskArn, service, resolve, reject, retries) { 45 | if (retries-- == 0) { 46 | return reject(new Error('Timed out when waiting for service to deploy')) 47 | } 48 | 49 | service.fetchEcsService().then(function(ecsService) { 50 | var myDeploy = null; 51 | var otherReleaseVersions = 0; 52 | var deployments = ecsService.deployments; 53 | for (var i = 0; i < deployments.length; i++) { 54 | if (deployments[i].taskDefinition == taskArn) { 55 | myDeploy = deployments[i]; 56 | } else { 57 | otherReleaseVersions += deployments[i].runningCount; 58 | } 59 | } 60 | 61 | if (myDeploy.status != 'PRIMARY') { 62 | reject(new Error('This deploy is no longer the primary deploy. Another deploy has taken precedence.')) 63 | return; 64 | } 65 | 66 | if (myDeploy.desiredCount == myDeploy.runningCount) { 67 | service._logProgress('New version is running'); 68 | if (otherReleaseVersions == 0) { 69 | service._logProgress('Old versions are no longer running') 70 | resolve(); 71 | } else { 72 | service.deployer.emit('waiting', { waiting: DEPLOY_WAIT_INTERVAL, status: 'RAMPING_DOWN_OLD_VERSION', service: service }) 73 | //service._logProgress('Old versions are still running, waiting ' + (DEPLOY_WAIT_INTERVAL / 1000) + ' seconds...') 74 | setTimeout(function() { 75 | _pollDeployment(taskArn, service, resolve, reject, retries); 76 | }, DEPLOY_WAIT_INTERVAL); 77 | } 78 | } else { 79 | service.deployer.emit('waiting', { waiting: DEPLOY_WAIT_INTERVAL, status: 'DEPLOYING_NEW_VERSION', service: service }) 80 | //service._logProgress('Its not deployed yet, waiting ' + (DEPLOY_WAIT_INTERVAL / 1000) + ' seconds...') 81 | setTimeout(function() { 82 | _pollDeployment(taskArn, service, resolve, reject, retries); 83 | }, DEPLOY_WAIT_INTERVAL); 84 | } 85 | }); 86 | } 87 | 88 | /** 89 | * Constructor function to create a new service. 90 | * 91 | * @param [Object] service The service definition. JSON. 92 | * @param [Deployer] deployer The deployer that is responsible for deploying this service. 93 | */ 94 | function Service(service, deployer) { 95 | this.service = service; 96 | this.ecs = new AWS.ECS(); 97 | this.autoScaling = new AWS.AutoScaling(); 98 | this.deployer = deployer; 99 | 100 | this.name = service.name; 101 | } 102 | 103 | /** 104 | * @scope private 105 | * Log/emit some progress message as we deploy. 106 | * 107 | * @param [String] msg The message to log/emit. 108 | */ 109 | Service.prototype._logProgress = function(msg) { 110 | console.log(msg); 111 | this.deployer.emit('progress', { 112 | msg: msg, 113 | service: this 114 | }); 115 | } 116 | 117 | /** 118 | * Download and parse the task definition from S3. 119 | * TODO: It would be nice if we supported more storage solutions than just S3. 120 | * 121 | * @param [String] url The S3 URL to the task definition json file. E.g., s3://key/my/task-definition.json 122 | * @return [Object] The task definition as a JSON object. 123 | */ 124 | Service.prototype.fetchTaskDefinition = function(taskDefinition) { 125 | var s = this; 126 | return new Promise(function(resolve, reject) { 127 | s.ecs.describeTaskDefinition({ taskDefinition: taskDefinition.family }, function(err, response) { 128 | if (err || !response.taskDefinition) { 129 | reject(err); 130 | return; 131 | } 132 | 133 | s._logProgress('Task definition retrieved'); 134 | 135 | // We only return parts of the task def response. 136 | resolve({ 137 | containerDefinitions: response.taskDefinition.containerDefinitions, 138 | family: response.taskDefinition.family, 139 | volumes: response.taskDefinition.volumes 140 | }); 141 | }); 142 | }); 143 | } 144 | 145 | /** 146 | * Register the task definition with ECS. Bump the version to the given version 147 | * in the meantime. 148 | * 149 | * @param [String] version The version of the docker image to use for the task definition. 150 | * @param [String] [imagePath] Repository where image is stored 151 | * @return [Promise] 152 | */ 153 | Service.prototype.registerTaskDefinition = function(version, imagePath) { 154 | var s = this; 155 | return this.fetchTaskDefinition(this.service.taskDefinition).then(function(taskDef) { 156 | return new Promise(function(resolve, reject) { 157 | // TODO: Revisit the imagePath method of skipping the update for certain container images. 158 | for (var i = 0; i < taskDef.containerDefinitions.length; i++) { 159 | // Update the image version number. 160 | // Maintain original behavior of updating all images if imagePath is not provided 161 | // Otherwise, only update if the given image repo is the same as what's currently used 162 | if ((typeof imagePath === 'undefined') || 163 | (imagePath === taskDef.containerDefinitions[i].image.split(':')[0])) { 164 | taskDef.containerDefinitions[i].image = _updateImageVersion(taskDef.containerDefinitions[i].image, version) 165 | } 166 | } 167 | 168 | s.ecs.registerTaskDefinition(taskDef, function(err, response) { 169 | if (err) { 170 | reject(err); 171 | return; 172 | } 173 | 174 | s._logProgress('Registered task def in ECS'); 175 | resolve(response.taskDefinition.taskDefinitionArn); 176 | }); 177 | }); 178 | }); 179 | }; 180 | 181 | /** 182 | * Update the ECS service to use the given task ARN. This registers our deploy with AWS/ECS. 183 | * 184 | * @param [String] taskArn The AWS ARN of the task definition to deploy. 185 | * @return [Promise] 186 | */ 187 | Service.prototype.updateEcsService = function(taskArn) { 188 | var s = this; 189 | return new Promise(function(resolve, reject) { 190 | s.ecs.updateService({ 191 | service: s.name, 192 | cluster: s.service.cluster, 193 | taskDefinition: taskArn 194 | }, function(err, data) { 195 | if (err) { 196 | reject(err); 197 | return; 198 | } 199 | 200 | s._logProgress('Updated service with new task def'); 201 | resolve(); 202 | }); 203 | }); 204 | }; 205 | 206 | /** 207 | * Scale the ECS cluster of EC2's up so that we have enough capacity for our deploy. 208 | * 209 | * @return [Promise] 210 | */ 211 | Service.prototype.scaleUp = function() { 212 | var s = this; 213 | return new Promise(function(resolve, reject) { 214 | s.fetchAutoScalingGroup().then(function(asg) { 215 | 216 | if (asg.DesiredCapacity === 0) { 217 | s._logProgress('Skipping auto-scale scale up since desired capacity is 0'); 218 | resolve(); 219 | return; 220 | } 221 | 222 | s.previousCapacity = asg.DesiredCapacity; 223 | var capacity = s.previousCapacity * 2; 224 | 225 | s.autoScaling.setDesiredCapacity({ 226 | AutoScalingGroupName: s.service.autoScaling.name, 227 | DesiredCapacity: capacity 228 | }, function(err, data) { 229 | if (err) { 230 | reject(err); 231 | return; 232 | } 233 | 234 | s._logProgress('Increased EC2 capacity to support deploy (cluster size = ' + capacity + ').') 235 | resolve(); 236 | }); 237 | }); 238 | }); 239 | }; 240 | 241 | /** 242 | * Wait for the deploy to complete. Deploy is complete when the new version is 243 | * running and all previous versions are not running. 244 | * 245 | * @param [String] taskArn The AWS ARN of the task we are deploying. 246 | * @param [Integer] maxAttempts The number of times we should check if the version is deployed. 247 | * @return [Promise] 248 | */ 249 | Service.prototype.waitForDeploy = function(taskArn, maxAttempts) { 250 | var service = this; 251 | if (!maxAttempts) maxAttempts = 100; 252 | return new Promise(function(resolve, reject) { 253 | _pollDeployment(taskArn, service, resolve, reject, maxAttempts); 254 | }); 255 | } 256 | 257 | /** 258 | * After deployment, scale the cluster back down. 259 | * 260 | * @return [Promise] 261 | */ 262 | Service.prototype.scaleDown = function() { 263 | // Scale down auto scaling groups 264 | var s = this; 265 | return new Promise(function(resolve, reject) { 266 | if (!s.previousCapacity) { 267 | // Never scaled up. 268 | resolve(); 269 | return; 270 | } 271 | 272 | s.autoScaling.setDesiredCapacity({ 273 | AutoScalingGroupName: s.service.autoScaling.name, 274 | DesiredCapacity: s.previousCapacity 275 | }, function(err, data) { 276 | if (err) { 277 | reject(err); 278 | return; 279 | } 280 | 281 | s._logProgress('Scaled cluster back down (cluster size = ' + s.previousCapacity + ')'); 282 | resolve(); 283 | }); 284 | }); 285 | } 286 | 287 | Service.prototype.isReady = function(version) { 288 | var deferreds = [ 289 | this.fetchTaskDefinition(this.service.taskDefinition), 290 | this.fetchEcsService() 291 | ]; 292 | 293 | if (this.service.autoScaling && this.service.autoScaling.name) { 294 | deferreds.push(this.fetchAutoScalingGroup()); 295 | } 296 | 297 | return Promise.all(deferreds); 298 | } 299 | 300 | Service.prototype.fetchEcsService = function() { 301 | var s = this; 302 | return new Promise(function(resolve, reject) { 303 | s.ecs.describeServices({ 304 | cluster: s.service.cluster, 305 | services: [ s.name ] 306 | }, function(err, data) { 307 | if (err) { 308 | reject(err); 309 | return; 310 | } 311 | 312 | if (data.services.length != 1) { 313 | reject(new Error('Unable to find ECS service')); 314 | return; 315 | } 316 | 317 | resolve(data.services[0]); 318 | }); 319 | }); 320 | } 321 | 322 | Service.prototype.fetchAutoScalingGroup = function() { 323 | var s = this; 324 | return new Promise(function(resolve, reject) { 325 | s.autoScaling.describeAutoScalingGroups({ 326 | AutoScalingGroupNames: [ s.service.autoScaling.name ], 327 | MaxRecords: 1 328 | }, function(err, data) { 329 | if (err) { 330 | reject(err); 331 | return; 332 | } 333 | 334 | if (data.AutoScalingGroups.length == 0) { 335 | reject(new Error('Unable to find auto scaling group')); 336 | return; 337 | } 338 | 339 | resolve(data.AutoScalingGroups[0]); 340 | }); 341 | }); 342 | } 343 | 344 | /** 345 | * Deploy the given version of this service. 346 | * 347 | * @param [String] version The version to deploy. 348 | * @param [String] [imagePath] Repository where image is stored 349 | * @return [Promise] 350 | */ 351 | Service.prototype.deploy = function(version, imagePath) { 352 | var service = this; 353 | return service.registerTaskDefinition(version, imagePath).then(function(taskArn) { 354 | return service.updateEcsService(taskArn).then(function() { 355 | if (service.service.autoScaling && service.service.autoScaling.name) { 356 | return service.scaleUp().then(function() { 357 | return service.waitForDeploy(taskArn).then(function() { 358 | return service.scaleDown().then(function() { 359 | return service; 360 | }) 361 | }) 362 | }) 363 | } else { 364 | return service.waitForDeploy(taskArn); 365 | } 366 | }) 367 | }) 368 | } 369 | 370 | module.exports = Service; 371 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecs-deployer", 3 | "version": "0.6.1", 4 | "description": "A set of utilities for automating deployments of docker images to ECS", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Brad Seefeld", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/DispatchBot/node-ecs-deployer" 14 | }, 15 | "dependencies": { 16 | "aws-sdk": "^2.6.8", 17 | "promise": "^7.0.4", 18 | "request": "^2.61.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | ajv@^4.9.1: 6 | version "4.11.8" 7 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" 8 | dependencies: 9 | co "^4.6.0" 10 | json-stable-stringify "^1.0.1" 11 | 12 | asap@~2.0.3: 13 | version "2.0.6" 14 | resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" 15 | 16 | asn1@~0.2.3: 17 | version "0.2.4" 18 | resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" 19 | dependencies: 20 | safer-buffer "~2.1.0" 21 | 22 | assert-plus@1.0.0, assert-plus@^1.0.0: 23 | version "1.0.0" 24 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 25 | 26 | assert-plus@^0.2.0: 27 | version "0.2.0" 28 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" 29 | 30 | asynckit@^0.4.0: 31 | version "0.4.0" 32 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 33 | 34 | aws-sdk@^2.6.8: 35 | version "2.122.0" 36 | resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.122.0.tgz#d40980fdb24a07db166de91cb8813f0dc640d7c6" 37 | dependencies: 38 | buffer "4.9.1" 39 | crypto-browserify "1.0.9" 40 | events "^1.1.1" 41 | jmespath "0.15.0" 42 | querystring "0.2.0" 43 | sax "1.2.1" 44 | url "0.10.3" 45 | uuid "3.0.1" 46 | xml2js "0.4.17" 47 | xmlbuilder "4.2.1" 48 | 49 | aws-sign2@~0.6.0: 50 | version "0.6.0" 51 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" 52 | 53 | aws4@^1.2.1: 54 | version "1.6.0" 55 | resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" 56 | 57 | base64-js@^1.0.2: 58 | version "1.2.1" 59 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" 60 | 61 | bcrypt-pbkdf@^1.0.0: 62 | version "1.0.2" 63 | resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" 64 | dependencies: 65 | tweetnacl "^0.14.3" 66 | 67 | boom@2.x.x: 68 | version "2.10.1" 69 | resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" 70 | dependencies: 71 | hoek "2.x.x" 72 | 73 | buffer@4.9.1: 74 | version "4.9.1" 75 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" 76 | dependencies: 77 | base64-js "^1.0.2" 78 | ieee754 "^1.1.4" 79 | isarray "^1.0.0" 80 | 81 | caseless@~0.12.0: 82 | version "0.12.0" 83 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 84 | 85 | co@^4.6.0: 86 | version "4.6.0" 87 | resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" 88 | 89 | combined-stream@^1.0.5, combined-stream@~1.0.5: 90 | version "1.0.5" 91 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" 92 | dependencies: 93 | delayed-stream "~1.0.0" 94 | 95 | core-util-is@1.0.2: 96 | version "1.0.2" 97 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 98 | 99 | cryptiles@2.x.x: 100 | version "2.0.5" 101 | resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" 102 | dependencies: 103 | boom "2.x.x" 104 | 105 | crypto-browserify@1.0.9: 106 | version "1.0.9" 107 | resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-1.0.9.tgz#cc5449685dfb85eb11c9828acc7cb87ab5bbfcc0" 108 | 109 | dashdash@^1.12.0: 110 | version "1.14.1" 111 | resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" 112 | dependencies: 113 | assert-plus "^1.0.0" 114 | 115 | delayed-stream@~1.0.0: 116 | version "1.0.0" 117 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 118 | 119 | ecc-jsbn@~0.1.1: 120 | version "0.1.2" 121 | resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" 122 | dependencies: 123 | jsbn "~0.1.0" 124 | safer-buffer "^2.1.0" 125 | 126 | events@^1.1.1: 127 | version "1.1.1" 128 | resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" 129 | 130 | extend@~3.0.0: 131 | version "3.0.2" 132 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 133 | 134 | extsprintf@1.3.0, extsprintf@^1.2.0: 135 | version "1.3.0" 136 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" 137 | 138 | forever-agent@~0.6.1: 139 | version "0.6.1" 140 | resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 141 | 142 | form-data@~2.1.1: 143 | version "2.1.4" 144 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" 145 | dependencies: 146 | asynckit "^0.4.0" 147 | combined-stream "^1.0.5" 148 | mime-types "^2.1.12" 149 | 150 | getpass@^0.1.1: 151 | version "0.1.7" 152 | resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" 153 | dependencies: 154 | assert-plus "^1.0.0" 155 | 156 | har-schema@^1.0.5: 157 | version "1.0.5" 158 | resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" 159 | 160 | har-validator@~4.2.1: 161 | version "4.2.1" 162 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" 163 | dependencies: 164 | ajv "^4.9.1" 165 | har-schema "^1.0.5" 166 | 167 | hawk@~3.1.3: 168 | version "3.1.3" 169 | resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" 170 | dependencies: 171 | boom "2.x.x" 172 | cryptiles "2.x.x" 173 | hoek "2.x.x" 174 | sntp "1.x.x" 175 | 176 | hoek@2.x.x: 177 | version "2.16.3" 178 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" 179 | 180 | http-signature@~1.1.0: 181 | version "1.1.1" 182 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" 183 | dependencies: 184 | assert-plus "^0.2.0" 185 | jsprim "^1.2.2" 186 | sshpk "^1.7.0" 187 | 188 | ieee754@^1.1.4: 189 | version "1.1.8" 190 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" 191 | 192 | is-typedarray@~1.0.0: 193 | version "1.0.0" 194 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 195 | 196 | isarray@^1.0.0: 197 | version "1.0.0" 198 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 199 | 200 | isstream@~0.1.2: 201 | version "0.1.2" 202 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 203 | 204 | jmespath@0.15.0: 205 | version "0.15.0" 206 | resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" 207 | 208 | jsbn@~0.1.0: 209 | version "0.1.1" 210 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" 211 | 212 | json-schema@0.2.3: 213 | version "0.2.3" 214 | resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" 215 | 216 | json-stable-stringify@^1.0.1: 217 | version "1.0.1" 218 | resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" 219 | dependencies: 220 | jsonify "~0.0.0" 221 | 222 | json-stringify-safe@~5.0.1: 223 | version "5.0.1" 224 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 225 | 226 | jsonify@~0.0.0: 227 | version "0.0.0" 228 | resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" 229 | 230 | jsprim@^1.2.2: 231 | version "1.4.1" 232 | resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" 233 | dependencies: 234 | assert-plus "1.0.0" 235 | extsprintf "1.3.0" 236 | json-schema "0.2.3" 237 | verror "1.10.0" 238 | 239 | lodash@^4.0.0: 240 | version "4.17.15" 241 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" 242 | 243 | mime-db@~1.29.0: 244 | version "1.29.0" 245 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" 246 | 247 | mime-types@^2.1.12, mime-types@~2.1.7: 248 | version "2.1.16" 249 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" 250 | dependencies: 251 | mime-db "~1.29.0" 252 | 253 | oauth-sign@~0.8.1: 254 | version "0.8.2" 255 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" 256 | 257 | performance-now@^0.2.0: 258 | version "0.2.0" 259 | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" 260 | 261 | promise@^7.0.4: 262 | version "7.3.1" 263 | resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" 264 | dependencies: 265 | asap "~2.0.3" 266 | 267 | punycode@1.3.2: 268 | version "1.3.2" 269 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" 270 | 271 | punycode@^1.4.1: 272 | version "1.4.1" 273 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 274 | 275 | qs@~6.4.0: 276 | version "6.4.0" 277 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" 278 | 279 | querystring@0.2.0: 280 | version "0.2.0" 281 | resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" 282 | 283 | request@^2.61.0: 284 | version "2.81.0" 285 | resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" 286 | dependencies: 287 | aws-sign2 "~0.6.0" 288 | aws4 "^1.2.1" 289 | caseless "~0.12.0" 290 | combined-stream "~1.0.5" 291 | extend "~3.0.0" 292 | forever-agent "~0.6.1" 293 | form-data "~2.1.1" 294 | har-validator "~4.2.1" 295 | hawk "~3.1.3" 296 | http-signature "~1.1.0" 297 | is-typedarray "~1.0.0" 298 | isstream "~0.1.2" 299 | json-stringify-safe "~5.0.1" 300 | mime-types "~2.1.7" 301 | oauth-sign "~0.8.1" 302 | performance-now "^0.2.0" 303 | qs "~6.4.0" 304 | safe-buffer "^5.0.1" 305 | stringstream "~0.0.4" 306 | tough-cookie "~2.3.0" 307 | tunnel-agent "^0.6.0" 308 | uuid "^3.0.0" 309 | 310 | safe-buffer@^5.0.1: 311 | version "5.1.1" 312 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 313 | 314 | safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: 315 | version "2.1.2" 316 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 317 | 318 | sax@1.2.1, sax@>=0.6.0: 319 | version "1.2.1" 320 | resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" 321 | 322 | sntp@1.x.x: 323 | version "1.0.9" 324 | resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" 325 | dependencies: 326 | hoek "2.x.x" 327 | 328 | sshpk@^1.7.0: 329 | version "1.16.1" 330 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" 331 | dependencies: 332 | asn1 "~0.2.3" 333 | assert-plus "^1.0.0" 334 | bcrypt-pbkdf "^1.0.0" 335 | dashdash "^1.12.0" 336 | ecc-jsbn "~0.1.1" 337 | getpass "^0.1.1" 338 | jsbn "~0.1.0" 339 | safer-buffer "^2.0.2" 340 | tweetnacl "~0.14.0" 341 | 342 | stringstream@~0.0.4: 343 | version "0.0.6" 344 | resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72" 345 | 346 | tough-cookie@~2.3.0: 347 | version "2.3.4" 348 | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" 349 | dependencies: 350 | punycode "^1.4.1" 351 | 352 | tunnel-agent@^0.6.0: 353 | version "0.6.0" 354 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 355 | dependencies: 356 | safe-buffer "^5.0.1" 357 | 358 | tweetnacl@^0.14.3, tweetnacl@~0.14.0: 359 | version "0.14.5" 360 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 361 | 362 | url@0.10.3: 363 | version "0.10.3" 364 | resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" 365 | dependencies: 366 | punycode "1.3.2" 367 | querystring "0.2.0" 368 | 369 | uuid@3.0.1: 370 | version "3.0.1" 371 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" 372 | 373 | uuid@^3.0.0: 374 | version "3.1.0" 375 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" 376 | 377 | verror@1.10.0: 378 | version "1.10.0" 379 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 380 | dependencies: 381 | assert-plus "^1.0.0" 382 | core-util-is "1.0.2" 383 | extsprintf "^1.2.0" 384 | 385 | xml2js@0.4.17: 386 | version "0.4.17" 387 | resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.17.tgz#17be93eaae3f3b779359c795b419705a8817e868" 388 | dependencies: 389 | sax ">=0.6.0" 390 | xmlbuilder "^4.1.0" 391 | 392 | xmlbuilder@4.2.1, xmlbuilder@^4.1.0: 393 | version "4.2.1" 394 | resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.2.1.tgz#aa58a3041a066f90eaa16c2f5389ff19f3f461a5" 395 | dependencies: 396 | lodash "^4.0.0" 397 | --------------------------------------------------------------------------------