<|start_filename|>lib/Opsworks.js<|end_filename|>
'use strict';

var Logger        = require('./logger');
var AWS           = require('aws-sdk');
AWS.config.region = 'us-east-1';

class Opsworks {
  constructor() {
    this.aws = new AWS.OpsWorks();
  }

  listStacks() {
    return new Promise((resolve, reject) => {
      this.aws.describeStacks({}, function(err, data) {
        if (err) reject(err, err.stack); // an error occurred
        else     resolve(data.Stacks);           // successful response
      });
    });
  }

  runCommands(stacks,type,args) {
    if(!stacks.length)
      throw new Error("No stacks matching your filters");
    
    Logger.info(`Running command ${type} on ${stacks.length} stacks`);

    var p = [];
    var StackById = {};

    stacks.forEach(stack => { 
      StackById[stack.StackId] = stack;
      p.push(this.runCommand(stack,type,args))
    });

    return Promise.all(p)
    .then(this.monitorDeployments.bind(this))
    .catch(function(e) {
      if(e.length) {
        var deployments = e;
        var f = 0;
        deployments.forEach(d => {
          var stack = StackById[d.StackId];
          if(d.Status == 'failed') {
            var logUrl = `https://console.aws.amazon.com/opsworks/home?region=${stack.Region}#/stack/${stack.StackId}/deployments/${d.DeploymentId}`;

            Logger.error(`Deployment failed on stack ${stack.Name}, logs: ${logUrl}`);
            f++;
          }
        });

        Logger.error(`Failed deployment on ${f}/${deployments.length} stacks`);
      } else {
        if(e.message.match(/at least an instance ID/)) {
          throw new Error("No running instance match your filters");
        } else {
          throw e;
        }
      }
    })
    .then(function() {
      Logger.info("Done");
    });
  }

  runCommand(stack,type,args) {

    var layers = [];
    var layersNames = [];
    stack.layers.forEach(layer => { 
      layers.push(layer.LayerId)
      layersNames.push(layer.Shortname);
    });

    Logger.debug(`Running command ${type} on ${stack.Name}, layers : ${layersNames.join(',')}`);

    var params = {
      Command: {
        Name: type
      },
      StackId: stack.StackId,
      LayerIds: layers
    }

    Logger.debug('Params:',params);

    if(type == 'execute_recipes') {
      params.Command.Args = {recipes: args};
    } else if(type == 'deploy') {
      stack.apps.forEach(app => {
        if(app.Shortname == args) {
          params.AppId = app.AppId;
        }
      });

      if(!params.AppId) {
        throw new Error(`Could not find app ${args} on stack ${stack.Name}`);
      }
    }

    return new Promise((resolve, reject) => {
      this.aws.createDeployment(params, function(err, data) {
        Logger.debug(err,data);
        if (err) reject(err, err.stack); // an error occurred
        else {
          resolve(data.DeploymentId);
        }
      });
    });
  }

  monitorDeployments(ids) {
    Logger.debug("Monitoring deployments of ",ids);
    var interval = null;
    return new Promise((resolve, reject) => {
      Logger.info("Monitoring deployments, please be patient...");
      this.checkDeployments(ids,resolve,reject);
    })
  }

  checkDeployments(ids,resolve,reject) {
    Logger.debug("Checking deployments status");
    var self = this;
    this.aws.describeDeployments({DeploymentIds: ids}, function(err, data) {
      if (err) reject(err, err.stack); // an error occurred
      else {
        Logger.debug("Deployments : ",data);
        var Deployments = data.Deployments;

        var counts = {};
        var total = Deployments.length;

        Deployments.forEach(d => {
          if(!counts[d.Status]) counts[d.Status] = 0;
          counts[d.Status]++;
        });

        if(counts['successful'] && counts['successful'] == total) {
          resolve(Deployments);
        }
        else if(!counts['running'])
          //If no deployment is running but not all are successful => error & all done.
          reject(Deployments);
        else
          setTimeout(self.checkDeployments.bind(self,ids,resolve,reject),10000);
      }
    });
  }

  // Adds instances to each layer
  fetchInstances(stacks) {
    return Promise.all(stacks.map(this.fetchInstancesForStack.bind(this)));
  }

  fetchInstancesForStack(stack) {
    Logger.debug(`Fetching instances for ${stack.Name} - ${stack.StackId}`);
    return new Promise((resolve, reject) => {
      this.aws.describeInstances({StackId: stack.StackId}, function(err, data) {
        Logger.debug(`Found ${data.Instances.length} instances for ${stack.Name} - ${stack.StackId}`)
        if (err) reject(err, err.stack); // an error occurred
        else     resolve(data.Instances);           // successful response
      });
    }).then(function(Instances) {
      Instances.forEach(i => {
        stack.layers.forEach(layer => {
          if(!layer.instances) layer.instances = [];
          if(layer.LayerId == i.LayerIds[0]) {
            layer.instances.push(i);
          }
        });
      });

      return stack;
    });
  }

  fetchDeployments(stacks) {
    return Promise.all(stacks.map(this.fetchDeploymentsForStack.bind(this)));
  }

  fetchDeploymentsForStack(stack) {
    Logger.debug(`Fetching deployments for ${stack.Name} - ${stack.StackId}`);
    return new Promise((resolve, reject) => {
      this.aws.describeDeployments({StackId: stack.StackId}, function(err, data) {
        if (err) reject(err, err.stack); // an error occurred
        else     resolve(data.Deployments);           // successful response
      });
    }).then(function(Deployments) {
      if(!stack.deployments)
        stack.deployments = [];
      Deployments.forEach(deployment => {
        stack.deployments.push(deployment)
      });

      return stack;
    });
  }

  fetchApps(stacks) {
    return Promise.all(stacks.map(this.fetchAppsForStack.bind(this)));
  }

  fetchElbs(stacks) {
    return Promise.all(stacks.map(this.fetchElbsForStack.bind(this)));
  }

  fetchAppsForStack(stack) {
    Logger.debug(`Fetching apps for ${stack.Name} - ${stack.StackId}`);
    return new Promise((resolve, reject) => {
      this.aws.describeApps({StackId: stack.StackId}, function(err, data) {
        if (err) reject(err, err.stack); // an error occurred
        else     resolve(data.Apps);           // successful response
      });
    }).then(function(Apps) {
      if(!stack.apps)
        stack.apps = [];
      Apps.forEach(app => {
        stack.apps.push(app)
      });

      return stack;
    });
  }

  fetchHealthForELB(ELB) {
    return new Promise((resolve, reject) => {
      var awsELB = new AWS.ELB({region: ELB.Region});
      awsELB.describeInstanceHealth({LoadBalancerName: ELB.ElasticLoadBalancerName},function(err,data) {
        if (err) reject(err, err.stack);
        else {
          ELB.InstanceStates = data.InstanceStates;
          resolve(ELB);
        }
      });
    });
  }

  fetchElbsForStack(stack) {
    var self = this;
    Logger.debug(`Fetching ELBs for ${stack.Name} - ${stack.StackId}`);
    return new Promise((resolve, reject) => {
      this.aws.describeElasticLoadBalancers({StackId: stack.StackId}, function(err, data) {
        if (err) reject(err, err.stack); // an error occurred
        else     resolve(data.ElasticLoadBalancers);           // successful response
      });
    }).then(function(Elbs) {
      return Promise.all(Elbs.map(self.fetchHealthForELB.bind(self)));
    }).then(function(Elbs) {
      if(!stack.elbs)
        stack.elbs = [];

      Elbs.forEach(elb => {
        stack.elbs.push(elb)
        if(stack.layers) {
          stack.layers.forEach(layer => {
            if(!layer.elbs)
              layer.elbs = [];

            if(elb.LayerId == layer.LayerId)
              layer.elbs.push(elb);
          });
        }
      });
      return stack;
    });
  }


  fetchLayers(stack) {
    //For arrays, find layers of all stacks
    if(stack.length) {
      return Promise.all(stack.map(this.fetchLayers.bind(this)));
    }

    return new Promise((resolve, reject) => {
      this.aws.describeLayers({StackId: stack.StackId}, function(err, data) {
        if (err) reject(err, err.stack); // an error occurred
        else {
          stack.layers = data.Layers;
          resolve(stack);
        }
      });
    });
  }

  /**
   * Given a list of stacks with layers, instances and ELBs,
   * Match the instance IDs of the ELB with the "friendly" names 
   * used in the opsworks stack.
   *  
   * @param  {array} stacks
   * @return {array} stacks
   */
  matchInstancesInElbs(stacks) {
    stacks.forEach(stack => {
      stack.layers.forEach(function(layer,index) {
        if(!layer.elbs) {
          delete stack.layers[index];
          return;
        }
        layer.elbs.forEach(elb => {
          elb.instances = [];
          elb.Ec2InstanceIds.forEach(id => {
            layer.instances.forEach(instance => {
              if(instance.Ec2InstanceId == id)
              {
                elb.InstanceStates.forEach(state => {
                  if(state.InstanceId == id) {
                    instance.state = state;
                    elb.instances.push(instance);
                  }
                });
              }
            });
          })
        })

      });
    });
    return stacks;
  }

  findStackByName(name) {
    return this.listStacks().then(stacks => {
      var foundStack = null;
      stacks.forEach(stack => {
        if(stack.Name == name) {
          foundStack = stack; 
          return;
        }
      });

      if(foundStack)
        return foundStack;
      else
        throw new Error("Cannot find stack "+name);
    });
  }
}

module.exports = Opsworks;


<|start_filename|>lib/StackList.js<|end_filename|>
'use strict';

var Logger = require('./logger');
class StackList {
  constructor (stacks) {
    this.stacks = stacks;
    this.stacks.forEach((stack, i) => {
      if(stack.CustomJson)
        var stackJSON = JSON.parse(stack.CustomJson);
      else
        var stackJSON = {};

      if(!stack.layers) stack.layers = [];
      stack.layers.forEach((layer, j) => {
        var layerJSON = (layer.CustomJson) ? JSON.parse(layer.CustomJson) : {};
        this.stacks[i].layers[j].custom_json  = Object.assign({}, stackJSON,layerJSON);
        this.stacks[i].layers[j].custom_json.id = {i:i,j:j};
      });
    });

  }

  applyFilters (filters) {
    Logger.debug(`Got ${this.stacks.length} stacks, applying filters`,filters);
    var seen = {};
    filters.forEach(filter => {
      var field = filter.split(':')[0];
      if(!seen[field])
        seen[field] = true;
      else {
        throw new Error("Cannot use the same filter twice.");
      }

      this.applyFilter(filter)
    });
    Logger.debug(`Got ${this.stacks.length} stacks after filters`,filters);
    return true;
  }

  applyFilter (filter) {
    var split = filter.split(':');

    if(split.length != 2)
      throw new Error("Incorrect filter (Format: name:value)");

    var field = split[0];
    var filter = split[1];
    var Regex = new RegExp('^'+filter.replace(/[^.]?\*/g,'.*')+'$');
    Logger.debug(`Filter ${filter} turned to regex ${Regex}`);

    //Stack-level filter by default
    var layerFilter = false;
    if(field == 'stack')
      field = 'Name';
    else if(field == 'region')
      field = 'Region';
    else {
      layerFilter = true;
    }

    var tmp = [];
    this.stacks.forEach(stack => {
      if(!layerFilter && stack[field] && stack[field].match(Regex))
        tmp.push(stack);
      else if(layerFilter) {
        var layers = [];
        stack.layers.forEach(layer => {
          if(field == 'layer' && layer.Shortname.match(Regex)) {
            layers.push(layer);
          } else if(layer.custom_json && layer.custom_json[field] && layer.custom_json[field].match(Regex)) {
            // It's a custom JSON filter.
            layers.push(layer)
          }
        });
        stack.layers = layers;

        //Push stack with only matching layers.
        if(stack.layers.length > 0)
          tmp.push(stack);
      }
    });

    this.stacks = tmp;
  }
}

module.exports = StackList;


<|start_filename|>lib/hierarchy.js<|end_filename|>
'use strict';

var colors    = require('colors');
var archy     = require('archy');

class Hierarchy {
  constructor(argv) {
    this.argv = argv;

    this.statusColors = {
      'booting': 'blue',
      'connection_lost': 'red',
      'online': 'green',
      'pending': 'blue',
      'rebooting': 'blue',
      'requested': 'blue',
      'running_setup': 'blue',
      'setup_failed': 'red',
      'shutting_down': 'red',
      'start_failed': 'red',
      'stop_failed': 'red',
      'stopped': 'reset',
      'stopping': 'blue',
      'terminated': 'grey',
      'terminating': 'blue'
    }
  }

     
  addInstanceToLayer(node,instance) {
    var instanceLabel = "";

    instanceLabel += `${instance.Hostname[this.statusColors[instance.Status]]}`;

    if(instance.Status !== 'online') {
      instanceLabel += ` - ${instance.Status[this.statusColors[instance.Status]]}`;
    }

    var type = instance.InstanceType || 'OnPremises' ;
    instanceLabel += ` - ${type}`;

    if(instance.PublicIp)
      instanceLabel += ` (${instance.PublicIp})`
    else if(instance.PrivateIp) {
      instanceLabel += ` (${instance.PrivateIp})`
    }

    node.nodes.push({label: instanceLabel, nodes: []});
  }

  addInstanceToELB(node,instance) {
    var instanceLabel = "";
    var details = false;

    if(instance.state.State === 'InService') {
      instanceLabel += "●".green+` ${instance.Hostname.green}`;
    } else {
      instanceLabel += "●".red+` ${instance.Hostname} - OutOfService`.red;
      details = true;
    } 

    var type = instance.InstanceType || 'OnPremises' ;
    instanceLabel += ` - ${type}`;

    if(instance.PublicIp)
      instanceLabel += ` (${instance.PublicIp})`
    else if(instance.PrivateIp) {
      instanceLabel += ` (${instance.PrivateIp})`
    }

    if(details) {
      instanceLabel += "\n"+`ReasonCode: ${instance.state.ReasonCode},`.red+"\n"+`Description: ${instance.state.Description}`.red;
    }

    node.nodes.push({label: instanceLabel, nodes: []});
  }

  printCSV(stacks) {
    stacks.forEach(stack => {
      stack.layers.forEach(layer => {
        layer.instances.forEach(instance => {
          console.log(`${stack.Name},${layer.Shortname},${instance.Hostname},${instance.Status},${instance.PublicIp || ''},${instance.PrivateIp || ''}`);
        });
      });
    });
  }

  /**
   * Displays a Hierarchy of stacks->layers->instances
   *
   * If the layers aren't attached to the Stacks object, they're omitted
   * If the instances aren't attached to the layers, they're omitted

   * @param  {object} stacks Stacks object from Opsworks.js
   * @return {void}
   */
  display(stacks) {
    var tree = {};
    tree.label = 'Stacks';
    tree.nodes = [];
    stacks.forEach(stack => {
      var stackLabel = `${stack.Name.bold.green.underline} - ${stack.Region}`;
      var stackNode = {label: stackLabel, nodes: []};

      if(!stack.apps) stack.apps = [];
      stack.apps.forEach(app => {
        var label = `${app.Shortname.green}`;
        for(var key in app.AppSource) {
          if(key == 'SshKey') continue;
          label += `\n${key}: ${app.AppSource[key]}`;
        }

        var appNode = {label: label, nodes: []};
        stackNode.nodes.push(appNode);
      });

      stack.layers.forEach(layer => {

        var layerNode = {label: layer.Shortname, nodes: []};
        stackNode.nodes.push(layerNode);
        if(!layer.instances) layer.instances = [];

        if(this.argv.command == 'instances')
          layer.instances.forEach(this.addInstanceToLayer.bind(this,layerNode));
        else if(this.argv.command == 'elbs') {
          layer.elbs.forEach(elb => {
            var elbLabel = `${elb.ElasticLoadBalancerName.magenta} - ${elb.Region}`
            var elbNode = {label: elbLabel, nodes: []};
            layerNode.nodes.push(elbNode);
            elb.instances.forEach(this.addInstanceToELB.bind(this,elbNode));
          });
        }
      });

      if(!stack.deployments) stack.deployments = [];
      var deployments = stack.deployments.slice(0,this.argv.n);
      deployments.forEach(deployment => {
        // console.log(deployment);
        var label = `${deployment.CreatedAt} - ${deployment.Command.Name.bold}`;
        var logUrl = false;

        switch(deployment.Status) {
          case 'running':
          label = label.blue;
          break;

          case 'failed':
          label = label.red;
          logUrl = `https://console.aws.amazon.com/opsworks/home?region=${stack.Region}#/stack/${stack.StackId}/deployments/${deployment.DeploymentId}`
          break;

          case 'successful':
          label = label.green;
          break;
        }

        if(logUrl) {
          label += "\n"+"Logs: ".red.bold + logUrl;
        }

        if(deployment.IamUserArn)
          label += `\nAuthor: ${deployment.IamUserArn.italic}`;
        else
          label += "\nAuthor: "+"Automatic AWS Deployment".italic;

        label += `\nStatus: ${deployment.Status}`;

        if(deployment.Duration) {
          label += `\nDuration: ${deployment.Duration}s`
        }

        if(deployment.Command.Name == 'execute_recipes') {
          var recipes = deployment.Command.Args.recipes.join(',');
          label += `\nRecipes: ${recipes}`
        }

        if(deployment.Comment) {
          label += `\nComment: ${deployment.Comment.italic}`
        }

        if(deployment.CustomJson) {
          label += `\nJSON: ${deployment.CustomJson.italic}`
        }

        var depNode = [];

        if(this.argv.i) {
          for(var i = 1; i < 5; i++) {
            if(Math.random() > 0.8) {
              depNode.push({label: `mediaserver${i}`.red.bold+' (fail)', nodes: []});
            } else
            depNode.push({label: `mediaserver${i}`.green+' (success)', nodes: []});
          }
        }
        stackNode.nodes.push({label:label, nodes: depNode});
      });

      tree.nodes.push(stackNode);
    });

    console.log(archy(tree));
  }
}

module.exports = Hierarchy;


<|start_filename|>lib/logger.js<|end_filename|>
var winston = require('winston');
var config  = require('./config');

var logger = module.exports = new (winston.Logger)({
  transports: [
    new (winston.transports.Console)({
      colorize: 'all',
      level: config.verbosity
    }),
    new (winston.transports.File)({
      filename: config.logfile,
      level: 'debug',
      json: false,
      options: {flags: 'w'}
    })
  ]
});


<|start_filename|>test/filters.js<|end_filename|>
'use strict';

var fs     = require('fs');
var StackList = require('./../lib/StackList');
var SL;

describe("Filters",function() {

  beforeEach(function() {
    var stacks = [];
    var stacksJSON = JSON.parse(fs.readFileSync("test/data/stacks.json").toString());
    stacksJSON.Stacks.forEach(stack => {
      var layersJSON = JSON.parse(fs.readFileSync("test/data/layers.json").toString());
      stack.layers = layersJSON.Layers;
      stacks.push(stack);
    });
    SL = new StackList(stacks);
  });



  describe("Built-in",function() {
    it("Based on region", function() {
      SL.applyFilters(['region:us-west-1']);
      SL.stacks.should.have.length(2);
    });

    it("Based on stack name", function() {
      SL.applyFilters(['stack:wordpress-production']);
      SL.stacks.should.have.length(1);
    });

    it("Based on layer name", function() {
      SL.applyFilters(['layer:database']);
      SL.stacks.should.have.length(3);
      SL.stacks.forEach(stack => {
        stack.layers.should.have.length(1);
      });
    });
  });
  
  describe("From custom JSON",function() {
    it("Custom filter test", function() {
      SL.applyFilters(['env:production']);
      SL.stacks.should.have.length(3);
    });

    it("Layer JSON should override Stack JSON", function() {
      //A stack has `env` set to test, but the layers override it.
      SL.applyFilters(['env:test']);
      SL.stacks.should.have.length(0);
    });

    it("Stack JSON should be queriable too", function() {
      //The final json of a layer should be the result of
      //merging the stack's and the layer's
      SL.applyFilters(['stackjsontest:somevalue']);
      SL.stacks.should.have.length(1);
    });

    it("Should not throw if the filter does not exist", function() {
      SL.applyFilters.bind(['thisdoesnotexist:somevalue']);
    });

    it("Should not throw if a layer has no custom_json", function() {
      var stacks = [];
      var stacksJSON = JSON.parse(fs.readFileSync("test/data/stacks.json").toString());
      stacksJSON.Stacks.forEach(stack => {
        var layersJSON = JSON.parse(fs.readFileSync("test/data/layers.json").toString());
        stack.layers = layersJSON.Layers;
        stacks.push(stack);
      });

      delete(stacks[0].layers[0].CustomJson);

      SL = new StackList(stacks);
      SL.applyFilters(['env:test']);

      SL.stacks.should.have.length(1);
      SL.stacks[0].layers.should.have.length(1);
    });
  });

  describe("Filtering",function() {
    it("Should throw using poorly formated filter", function() {
      SL.applyFilters.bind(SL,['thisisnotafilter']).should.throw(/Incorrect filter/);
      SL.applyFilters.bind(SL,['that:is:notright']).should.throw(/Incorrect filter/);
    });

    it("Should throw using the same filter twice", function() {
      SL.applyFilters.bind(SL,['env:production','env:staging']).should.throw(/same filter/);
    });

    it("Should omit stacks with no layers", function() {
      SL.applyFilters(['layer:thisdoesnotexist']);
      SL.stacks.should.have.length(0);
    });

    it("Combined filters", function() {
      SL.applyFilters(['region:us-west*','stack:wordpress*','layer:database']);
      SL.stacks.should.have.length(2);
      SL.stacks.forEach(stack => {
        stack.layers.should.have.length(1);
      })
    });

    it("Wildcard before", function() {
      SL.applyFilters(['stack:*']);
      SL.stacks.should.have.length(3);

      SL.applyFilters(['stack:*-production']);
      SL.stacks.should.have.length(1);
    });

    it("Wildcard after", function() {
      SL.applyFilters(['stack:wordp*']);
      SL.stacks.should.have.length(3);      

      SL.applyFilters(['region:us-west*']);
      SL.stacks.should.have.length(2);      
    });

    it("Multiple wildcards", function() {
      SL.applyFilters(['stack:*-*']);
      SL.stacks.should.have.length(3);

      SL.applyFilters(['region:us-*-1']);
      SL.stacks.should.have.length(3);
    });
  });
});


<|start_filename|>lib/commandrunner.js<|end_filename|>
'use strict';

var config    = require('./config');
var Logger    = require('./logger');
var OpsWorks  = require('./Opsworks');
var StackList = require('./StackList');
var Prompt    = require('./prompt');
var hierarchy = require('./hierarchy');

class CommandRunner {
  constructor(argv) {
    this.argv = argv;
    this.hierarchy = new hierarchy(argv);
    Logger.debug("Argv : ",this.argv);
    this.OpsWorks = new OpsWorks();

    if(argv.v) {
      Logger.transports.console.level = 'debug';
    }
  }

  /**
   * Point of entry, uses the argv of this run to determine what to do
   *
   * If a command is specified, called the associated method
   *   Ex: `stacks` calls stacks()
   * Otherwise, call runCli() to get the interactive cli
   * 
   * @return {void}
   */
  runCommand() {
    Logger.debug("Running command",this.argv);

    if(this.argv._.length > 1) {
      var err = new Error("Got more than one command : "+this.argv._.join(', '));
      Logger.error(err.message);
      throw err;
    }

    if(this.argv.command) {
      this.filters = (this.argv.f) ? this.argv.f.split(',') : [];
      return this[this.argv.command]();
    } else {
      this.filters = [];
      return this.prompt();
    }
  }

  deployments() {
    var filters = this.filters;
    Logger.debug("Getting the list of deployments");
    return this.OpsWorks.listStacks()
    .then(this.OpsWorks.fetchLayers.bind(this.OpsWorks))
    .then(function(stacks) {
      var SL = new StackList(stacks);
      filters.forEach(filter => {
        if(filter.split(':')[0] === 'layer') {
          Logger.warn("You specified a layer filter, deployments are per stack and not per layer.\nFetching deployments for stacks that match your filters.");
        }
      });
      SL.applyFilters(filters);
      return SL.stacks;
    })
    .then(this.OpsWorks.fetchDeployments.bind(this.OpsWorks))
    .then(function(stacks) {
      stacks = stacks.map(stack => { stack.layers = []; return stack; });
      return stacks;
    })
    .then(this.hierarchy.display.bind(this.hierarchy))
  }

  stacks() {
    var filters = this.filters;
    Logger.debug("Getting the list of stacks");
    return this.OpsWorks.listStacks()
    .then(this.OpsWorks.fetchLayers.bind(this.OpsWorks))
    .then(function(stacks) {
      var SL = new StackList(stacks);
      SL.applyFilters(filters);
      return SL.stacks;
    })
    .then(this.hierarchy.display.bind(this.hierarchy))
  }

  instances() {
    var self = this;
    var filters = this.filters;
    Logger.debug("Getting the list of instances");
    return this.OpsWorks.listStacks()
    .then(this.OpsWorks.fetchLayers.bind(this.OpsWorks))
    .then(function(stacks) {
      var SL = new StackList(stacks);
      SL.applyFilters(filters);
      return SL.stacks;
    })
    .then(this.OpsWorks.fetchInstances.bind(this.OpsWorks))
    .then(function(stacks) {
      if(self.argv.c) {
        return self.hierarchy.printCSV(stacks);
      } else {
        return self.hierarchy.display(stacks);
      }
    })
  }

  elbs() {
    var filters = this.filters;
    Logger.debug("Getting the list of ELBs");
    return this.OpsWorks.listStacks()
    .then(this.OpsWorks.fetchLayers.bind(this.OpsWorks))
    .then(function(stacks) {
      var SL = new StackList(stacks);
      SL.applyFilters(filters);
      return SL.stacks;
    })
    .then(this.OpsWorks.fetchElbs.bind(this.OpsWorks))
    .then(this.OpsWorks.fetchInstances.bind(this.OpsWorks))
    .then(this.OpsWorks.matchInstancesInElbs.bind(this.OpsWorks))
    .then(this.hierarchy.display.bind(this.hierarchy))
  }

  apps() {
    var filters = this.filters;
    Logger.debug("Getting the list of stacks");
    return this.OpsWorks.listStacks()
    .then(this.OpsWorks.fetchApps.bind(this.OpsWorks))
    .then(function(stacks) {
      var SL = new StackList(stacks);
      SL.applyFilters(filters);
      return SL.stacks;
    })
    .then(this.hierarchy.display.bind(this.hierarchy))
  }

  getFilteredStacks() {
    var filters = this.filters;
    Logger.debug("Getting the list of stacks");
    return this.OpsWorks.listStacks()
    .then(this.OpsWorks.fetchLayers.bind(this.OpsWorks))
    .then(function(stacks) {
      var SL = new StackList(stacks);
      SL.applyFilters(filters);
      return SL.stacks;
    });
  }

  // Commands : deploy,update,configure,setup
  update() {
    return this.getFilteredStacks()
    .then(this.askConfirmation.bind(this))
    .then(stacks => {
      return this.OpsWorks.runCommands(stacks,'update_custom_cookbooks');
    });
  }

  deploy() {
    return this.getFilteredStacks()
    .then(this.OpsWorks.fetchApps.bind(this.OpsWorks))
    .then(this.askConfirmation.bind(this))
    .then(stacks => {
      if(this.argv.u) {
        return this.OpsWorks.runCommands(stacks,'update_custom_cookbooks')
        .then(function() { return stacks; })
      } else {
        return stacks;
      }
    })
    .then(stacks => {
      return this.OpsWorks.runCommands(stacks,'deploy',this.argv.application_name);
    });
  }

  recipes() {
    var recipes = this.argv.recipes_list.split(',');
    return this.getFilteredStacks()
    .then(this.OpsWorks.fetchApps.bind(this.OpsWorks))
    .then(this.askConfirmation.bind(this))
    .then(stacks => {
      if(this.argv.u) {
        return this.OpsWorks.runCommands(stacks,'update_custom_cookbooks')
        .then(function() { return stacks; })
      } else {
        return stacks;
      }
    })
    .then(stacks => {
      return this.OpsWorks.runCommands(stacks,'execute_recipes',recipes);
    });
  }

  configure() {
    return this.getFilteredStacks()
    .then(this.askConfirmation.bind(this))
    .then(stacks => {
      if(this.argv.u) {
        return this.OpsWorks.runCommands(stacks,'update_custom_cookbooks')
        .then(function() { return stacks; })
      } else {
        return stacks;
      }
    })
    .then(stacks => {
      return this.OpsWorks.runCommands(stacks,'configure');
    });
  }

  setup() {
    return this.getFilteredStacks()
    .then(this.askConfirmation.bind(this))
    .then(stacks => {
      if(this.argv.u) {
        return this.OpsWorks.runCommands(stacks,'update_custom_cookbooks')
        .then(function() { return stacks; })
      } else {
        return stacks;
      }
    })
    .then(stacks => {
      return this.OpsWorks.runCommands(stacks,'setup');
    });
  }

  prompt() {
    var prompt = new Prompt(this);
    return prompt.start();
  }  

  askConfirmation(stacks) {
    if(this.argv.y) {
      Logger.debug("Skipping confirmation because -y was supplied");
      return stacks;
    } else {
      var prompt = new Prompt(this);
      return prompt.askConfirmation(stacks,this.argv);
    }
  }
}

module.exports = CommandRunner;


<|start_filename|>lib/config.js<|end_filename|>
var yaml_config = require('node-yaml-config');
 
var config = yaml_config.load(__dirname + '/../config/config.yml');

module.exports = config;


<|start_filename|>index.js<|end_filename|>
module.exports = {
    CommandRunner : require('./lib/commandrunner'),
    OpsWorks : require('./lib/Opsworks'),
    StackList : require('./lib/StackList'),
    Prompt : require('./lib/prompt'),
    Logger : require('./lib/logger')
}


<|start_filename|>test/opsworks.js<|end_filename|>
'use strict';

var config   = require('./../lib/config');
var Logger   = require('./../lib/logger');

Logger.transports.console.level = 'error';

var fs       = require('fs');
var sinon    = require('sinon');
require('sinon-as-promised')

var OpsWorks = require('./../lib/Opsworks');


describe("OpsWorks", function() {
  var cli;
  beforeEach(function() {
    cli = new OpsWorks();

    var stacksJSON = JSON.parse(fs.readFileSync("test/data/stacks.json").toString());
    var layersJSON = JSON.parse(fs.readFileSync("test/data/layers.json").toString());

    var stub = sinon.stub(cli,'listStacks');
    stub.resolves(stacksJSON.Stacks);

    var stub2 = sinon.stub(cli.aws,'describeLayers', function(params,callback){
      callback(null,JSON.parse(fs.readFileSync("test/data/layers.json").toString()));
    });
  });

  describe("#findStackByName", function() {
    it("Should find stack by name", function() {
      return cli.findStackByName('wordpress-production').should.be.fulfilled()
      .then(stack => {
        stack.Name.should.be.equal('wordpress-production');
      });
    });

    it("Should throw when the stack is not found", function() {
      return cli.findStackByName('wordpress-notexisting').should.be.rejectedWith(/Cannot find/);
    });
  });

  describe("#fetchLayers", function() {
    it("Should add the layers to a stack object", function() {
      return cli.findStackByName('wordpress-production')
      .then(cli.fetchLayers.bind(cli))
      .then(stack => {
        stack.layers.should.have.length(2);
      });
    });

    it("Should add the layers to multiple stacks", function() {
      return cli.listStacks()
      .then(cli.fetchLayers.bind(cli))
      .then(stacks => {
        stacks.should.have.length(3);
        for (var i = stacks.length - 1; i >= 0; i--) {
          stacks[i].layers.should.have.length(2);
        }
      });
    });
  });

  describe("#fetchInstances", function() {
    beforeEach(function() {
      var stub2 = sinon.stub(cli.aws,'describeInstances');
      stub2.callsArgWith(1,null,{Instances: [{
        InstanceId: Math.random(),
        LayerIds: ['123456789-random-id-obfuscated']
      }]});
    });

    it("Should attach the instances to the layers", function() {
      return cli.listStacks()
      .then(cli.fetchLayers.bind(cli))
      .then(cli.fetchInstances.bind(cli))
      .then(stacks => {
        stacks[0].layers.should.have.length(2);
        stacks[0].layers[0].instances.should.have.length(1);
        stacks[0].layers[1].instances.should.have.length(0);
      });
    });

    it("Should not attach instances with no matching layers", function() {
      return cli.listStacks()
      .then(cli.fetchLayers.bind(cli))
      .then(function(stacks) {
        stacks[0].layers = stacks[0].layers.slice(0,1);
        return stacks;
      })
      .then(cli.fetchInstances.bind(cli))
      .then(stacks => {
        stacks[0].layers.should.have.length(1);
        stacks[0].layers[0].instances.should.have.length(1);
      });
    });
  });

  describe("#fetchApps", function() {
    beforeEach(function() {
      var stubApps = sinon.stub(cli.aws,'describeApps', function(params,callback){
        callback(null,JSON.parse(fs.readFileSync("test/data/apps.json").toString()));
      });
    });

    it("Should attach the apps to the stack", function() {
      return cli.listStacks()
      .then(cli.fetchApps.bind(cli))
      .then(stacks => {
        stacks[0].apps.should.have.length(2);
        stacks[1].apps.should.have.length(2);
        stacks[0].apps[0].Shortname.should.be.equal('dummyapp1');
        stacks[0].apps[1].Shortname.should.be.equal('dummyapp2');
      });
    });
  });

  describe("#fetchElbs", function() {
    beforeEach(function() {
      var stubElbs = sinon.stub(cli.aws,'describeElasticLoadBalancers', function(params,callback){
        callback(null,JSON.parse(fs.readFileSync("test/data/elbs.json").toString()));
      });

      var stub2 = sinon.stub(cli.aws,'describeInstances');
      stub2.callsArgWith(1,null,{Instances: [
        {
          InstanceId: 'i-instance1',
          Ec2InstanceId: 'i-instance1',
          Name: 'instance1',
          LayerIds: ['123456789-random-id-obfuscated']
        },
        {
          InstanceId: 'i-instance2',
          Ec2InstanceId: 'i-instance2',
          Name: 'instance2',
          LayerIds: ['123456789-random-id-obfuscated']
        },
        {
          InstanceId: 'i-instance3',
          Ec2InstanceId: 'i-instance3',
          Name: 'instance3',
          LayerIds: ['123456789-random-id-obfuscated']
        },
        {
          InstanceId: 'i-instance4',
          Ec2InstanceId: 'i-instance4',
          Name: 'instance4',
          LayerIds: ['123456789-random-id-obfuscated']
        }
      ]});

      cli.fetchHealthForELB = function(ELB) {
        // var elbs = JSON.parse(fs.readFileSync("test/data/elbs.json").toString());
        // var elb = elbs.ElasticLoadBalancers[0];
        var health = JSON.parse(fs.readFileSync("test/data/healthELB.json").toString());
        ELB.InstanceStates = health.InstanceStates;
        return ELB;
      }
      // var stub3 = sinon.stub(cli,'fetchHealthForELB');
      // stub3.resolves({});
    });

    it("Should attach the Elbs to the corresponding layer", function() {
      return cli.listStacks()
      .then(cli.fetchLayers.bind(cli))
      .then(cli.fetchElbs.bind(cli))
      .then(stacks => {
        stacks[0].layers[0].elbs.should.have.length(1);
        stacks[0].layers[1].elbs.should.have.length(0);
      });
    });

    it("Should match instances to their ELBs", function() {
      return cli.listStacks()
      .then(cli.fetchLayers.bind(cli))
      .then(cli.fetchInstances.bind(cli))
      .then(cli.fetchElbs.bind(cli))
      .then(cli.matchInstancesInElbs.bind(cli))
      .then(stacks => {
        stacks[0].layers[0].elbs.should.have.length(1);
        stacks[0].layers[1].elbs.should.have.length(0);
        stacks[0].layers[0].elbs[0].instances.should.have.length(4);
      });
    });
  });

  describe("#fetchDeployments", function() {
    beforeEach(function() {
      var stubDeployment = sinon.stub(cli.aws,'describeDeployments');
      stubDeployment.callsArgWith(1,null,{Deployments: [{
        Status: 'running'
      },{
        Status: 'failed'
      }]});


    });

    it("Should attach the deployments to the stack", function() {
      return cli.listStacks()
      .then(cli.fetchDeployments.bind(cli))
      .then(stacks => {
        stacks[0].deployments.should.have.length(2);
        stacks[1].deployments.should.have.length(2);
      });
    });
  });

  describe("#runCommands / #monitorDeployment", function() {
    var clock;
    var stub2;

    beforeEach(function() {
      var stub = sinon.stub(cli.aws,'createDeployment');
      stub.onCall(0).callsArgWith(1,null,{DeploymentId: 'fake-deployment-id-0'});
      stub.onCall(1).callsArgWith(1,null,{DeploymentId: 'fake-deployment-id-1'});
      stub.onCall(2).callsArgWith(1,null,{DeploymentId: 'fake-deployment-id-2'});

      var count = 0;
      stub2 = sinon.stub(cli.aws,'describeDeployments',(params,callback) => {
        var status = (count++ == 5) ? 'successful' : 'running';
        var res = [];

        params.DeploymentIds.forEach((id,i) => {
          if(i == 1)
            status = 'successful';
          res.push({DeploymentId: id, Status: status})
        });

        callback(null,{Deployments: res});

        clock.tick(10000);
      });

      clock = sinon.useFakeTimers();
    });

    afterEach(function() {
      clock.restore();
    })

    it("Should check the deployment's status at regular intervals", function() {
      return cli.findStackByName('wordpress-production')
      .then(cli.fetchLayers.bind(cli))
      .then(stack => {
        return cli.runCommand(stack,'fake_command');
      })
      .then(id => {
        var p = cli.monitorDeployments([id]);
        return p;
      });
    });

    it("Should be able to run commands across multiple stacks", function() {
      return cli.listStacks()
      .then(cli.fetchLayers.bind(cli))
      .then(stacks => {
        var p = cli.runCommands(stacks,'fake_command');
        return p;
      })
      .then(function() {
        //describeDeployments should have been called 6 times
        stub2.callCount.should.equal(6);
      });
    });
  });
});


<|start_filename|>lib/prompt.js<|end_filename|>
'use strict';

var config    = require('./config');
var Logger    = require('./logger');
var OpsWorks  = require('./Opsworks');
var StackList = require('./StackList');
var inquirer  = require('inquirer');

var colors    = require('colors');

class Prompt {
  constructor(CommandRunner) {
    this.CommandRunner = CommandRunner;
    this.OpsWorks = new OpsWorks();
  }

  start() {
    var self = this;

    return inquirer.prompt([
      {
        type: 'list',
        name: 'command',
        message: 'What do you want to do?',
        choices: [
          {name: 'List stacks', value: 'stacks'},
          {name: 'List instances', value: 'instances'},
          {name: 'Inspect ELBs', value: 'elbs'},
          {name: 'Update cookbooks', value: 'update_custom_cookbooks'},
          {name: 'Deploy code', value: 'deploy'},
          {name: 'Run Configure', value: 'configure'},
          {name: 'Run Setup', value: 'setup'}
        ]
      }
    ]).then(function (answer) {
      switch(answer.command) {
        case 'stacks':
        return self.CommandRunner.stacks();

        case 'instances':
        return self.pickStack(answer)
        .then(answers => {
          self.CommandRunner.argv.command = 'instances';
          self.CommandRunner.filters = [`stack:${answers.stack.Name}`];
          return self.CommandRunner.instances();
        });

        case 'elbs':
        return self.pickStack(answer)
        .then(answers => {
          self.CommandRunner.argv.command = 'elbs';
          self.CommandRunner.filters = [`stack:${answers.stack.Name}`];
          return self.CommandRunner.elbs();
        });

        case 'deploy':
        return self.pickStack(answer)
        .then(self.pickApp.bind(self))
        .then(self.pickLayers.bind(self))
        .then(answers => {
          return self.OpsWorks.runCommands([answers.stack],answers.command,answers.app);
        });

        default:
        return self.pickStack(answer)
        .then(self.pickLayers.bind(self))
        .then(answers => {
          var filteredLayers = [];
          var stack = answers.stack;

          stack.layers.forEach(layer => {
            if(answers.layers.indexOf(layer.Shortname) >= 0)
              filteredLayers.push(layer);
          });

          stack.layers = filteredLayers;
          return self.askConfirmation([stack],{command: answers.command})
          .then(function(stacks) {
            return self.OpsWorks.runCommands(stacks,answers.command);
          });
        });
      }
    });
  }

  askConfirmation(stacks,argv) {
    var targets = [];
    stacks.forEach(stack => {
      var layers = [];
      stack.layers.forEach(layer => {
        layers.push(layer.Shortname);
      });
      targets.push(`${stack.Name.green}:${layers.join(', ')}`);
    });

    var commands = [];
    if(argv.u) {
      commands.push("update")
    }

    commands.push(argv.command);

    console.log(`/!\\ Running ${commands.join(', ').red} on stacks /!\\`.bold);
    console.log(targets.join("\n"),"\n");

    return inquirer.prompt([
      {
        type: 'confirm',
        name: 'confirm',
        message: 'Confirm the command?'
      }
    ]).then(function(answers) {
      if(!answers.confirm)
        throw new Error("Command aborted");
      else
        return stacks;
    });
  }

  pickStack(answers) {
    return this.OpsWorks.listStacks()
    .then(stacks => {
      var choices = [];
      stacks.forEach(stack => choices.push({
        name: stack.Name,
        value: stack
      }));

      return inquirer.prompt([
      {
        type: 'list',
        name: 'stack',
        message: 'On which stack?',
        choices: choices
      }])
      .then(answer => {
        answers.stack = answer.stack;
        return answers;
      });
    });
  }

  pickApp(answers) {
    return this.OpsWorks.fetchAppsForStack(answers.stack)
    .then(this.OpsWorks.fetchLayers.bind(this.OpsWorks))
    .then(stack => {
      answers.stack = stack;

      var choices = [];
      stack.apps.forEach(app => choices.push({
        name: app.Name,
        value: app.Shortname
      }));

      if(choices.length == 0)
        throw new Error(`The stack ${stack.Name} has no App attached to it.`);

      return inquirer.prompt([
      {
        type: 'list',
        name: 'app',
        message: 'Deploy which app?',
        choices: choices
      }])
      .then(answer => {
        answers.app = answer.app;
        return answers;
      });
    });
  }

  pickLayers(answers) {
    return this.OpsWorks.fetchLayers(answers.stack)
    .then(stack => {
      var choices = [];
      stack.layers.forEach(layer => choices.push({
        name: layer.Name,
        value: layer.Shortname
      }));

      return inquirer.prompt([{
        type: 'checkbox',
        message: 'Select layers',
        pageSize: 102,
        name: 'layers',
        choices: choices,
        validate: function (answer) {
          if (answer.length < 1) {
            return 'You must choose at least one layer.';
          }
          return true;
        }
      }]).then(function(answer) {
        answers.layers = answer.layers;
        return answers;
      });
    });
  }
}

module.exports = Prompt;
