Friday, May 12, 2017

Bouquet Binary

I recently dusted off an old project of mine called Bouquet. It basically helps me quickly
architect and design systems. See my original blog here. This blog focuses on the Command Line Interface (CLI) aspect of the Bouquet Project.  The project is hosted on GitHub herehttps://github.com/madajaju/bouquet

Binary setup

There are several different kinds of binary files that are used in the bouquet pattern.
  1. Top Level Command script - "projectName"
  2. Actor Command Script - "projectName-actorName"
  3. Subsystem Command Script - "projectName-subsystemName"
  4. Command Script - "projectName-actorName-command", "projectName-subsystemName-command", or "projectName-command"
The goal here is that we have a consistent command line interface. For example in the project named caade the following are some commands

# caade init // High level scenario
# caade stack up // Subsystem Command
# caade dev ps // Actor Command

Top Level Command Script

There should be one system command that contains all of the commands for the system using the commander package.
  • The name of the file should be "projectName" in the bin directory.
  • The for each actor there should be a command for the actor. This will give a command line interface for each actor
  • There should be a command for each subsystem as well. This will give the ability for each subsystem to have a CLI.
  • There should be a command for each of the top level scenarios for the system. The following is an example of this top level command file
In this case "caade"
#!/usr/bin/env node

var program = require('commander');

program
  .version("0.2.0")
  // Actors
  .command('app <command> <applicationName>', 'Work with applications')
  .command('stack <command> <stackName>', 'Work with applications')
  .command('adm <command> <stackName>', 'Work with applications')
  // SubSystems 
  .command('policy <command> <policyName>', 'Work with Policies')
  .command('cloud <command> <cloudName>', 'Work with Clouds')
  .command('environment <command> <EnvironmentName>', 'Work with applications')
  .command('service <command> <EnvironmentName>', 'Work with servioes')
  .command('user <command> <UserName>', 'Work with Users')
  // Scenarios
  .command('init', 'initalize Caade on your machine')
  .command('up [service-name]', 'Launch an application in a specific environment')
  .command('update [service-name]', 'Update web service with new code')
  .command('run <command>', 'Run a command in specified environment')
  .command('ps <command>', 'List processes for the application')
  .command('kill <serviceName>', 'Kill specific service for the application')
  .command('logs [serviceName]', 'Get logs of the application')
  .command('deploy', 'Deploy an application')
  .parse(process.argv);

Actor Command Script

This is very much like the Top level command script but limits the commands to the actor The file is named "projectName-actorName" a simple example follows.
In this case "caade-app"
#!/usr/bin/env node

var program = require('commander');

program
  .version("0.2.0")
  .command('create <application name>', 'Create an application')
  .command('get <application name>', 'Create an application')
  .command('ls', 'List my applications')
  .command('remove <application name>', 'Remove my application')
  .command('show <application name>', 'show details about my application')
  .parse(process.argv);
The Controller for this might look something like this AppController.js
module.exports = {

  create: function (req, res) {
    var name = "";  // Default
    var stackName = "";  // Default
    if (req.query.name) {
      name = req.query.name;
    }
    else {
      // Return Error "No Application Name specified"
      return res.json({error: "No Application Name specified!"})
    }
    if (req.query.stack) {
      stackName = req.query.stack;
    }
    else {
      // Return error with "No Application Stack specified"
      return res.json({error: "No Application Stack specified!"})
    }
    return Application.find({name: name})
      .then(function (app) {
        res.json({application: app});
      });
  },
  get: function (req, res) { ... },
  delete: function (req, res) { ... },
  list: function (req, res) { ... },
  show: function (req, res) { ... },
  ps: function (req, res) { ... },
  up: function (req, res) { ... },
  kill: function (req, res) { ... }
};

Subsytem Command Script

This is very much like the Top level command script but limits the commands to the subsystem The file is named "projectName-subsystemName" a simple example follows.
In this case "caade-cloud"
#!/usr/bin/env node

var program = require('commander');

program
  .version("0.2.0")
  .command('create <cloudName>', 'Attach a Cloud')
  .command('ls', 'List the Clouds attached')
  .command('remove <cloudName>', 'Remove a Cloud')
  .command('show <cloudName>', 'Show details about a Cloud')
  .parse(process.argv);

Command Script

Command scripts are where everything really happens. The previous scripts just setup for accessing the command scripts. The naming convention of the command scripts follows the actor and subsystem nomenclature "projectName-actorName-command", "projectName-subsystemName-command", or "projectName-command". The trick of the command is to connect to the rest interface of the system. This should coorespond to the controller with a simalar name. For example if you have actor command script then there should be a cooresponding controller for the actor. This way the REST and CLI APIs are consistent.
The following is an example of a simple Command Script that accesses the rest interface. In this case it shows information about a stack in the system
#!/usr/bin/env node

var program = require('commander');
var Client = require('node-rest-client').Client; // Needed to access the REST Interfacce.
var config = require('./system-config'); // Contains the URL to connect to for the REST Interface
var _ = require('lodash');

var client = new Client();

program
  .option('-v, --version <versionNumber>', 'Show an application stack with version')
  .parse(process.argv);

var name = program.args;

// Create the REST Command
var url = config.caadeUrl + "/stack/show?";

if(name) {
  url += "name=" + name[0];
}

if (program.version) {
  url += "&version=" + program.version;
}
// Call the REST Interface via HTTP Client.
client.get(url, function (data, response) {
  // parsed response body as js object
  if(data.error) {
    console.error(data.error);
  }
  else {
    console.log(data.stack);
    console.log("Name:" + data.stack.name + "\tVersion: " + data.stack.version);
  }
});
Another thing that I found useful was having the ability to include the ability to allow the user to add a file as an argument to the CLI. This is good for passing in yaml or json files that can be passed into the Controller. In this case I am passing in a yaml file.
#!/usr/bin/env node

var program = require('commander');
var Client = require('node-rest-client').Client; // Access the REST interface
var config = require('./caade-config');
var YAML = require('yamljs'); // Parse a YAML file

var client = new Client();

program
  .option('-f, --filename <filename>', 'Create an application stack from file')
  .option('-e, --env <environmentName>', 'Create an application stack for the environment')
  .parse(process.argv);

var name = program.args;

var url = config.caadeUrl + "/stack/create";
// Taking a YAMLfile and converting to JSON and then passing it into the REST interface.
var args = { headers: {"Content-Type": "application/json"}, data: {} }

if(name) {
  args.data.name = name[0];
}

var definition = {};
// Load the YAML file from the local drive and convert it to JSON.
if (program.filename) {
  args.data.definition = YAML.load(program.filename);
}

if (program.env) {
  args.data.env = program.env;
}

client.post(url, args, function (data, response) {
  // parsed response body as js object
  if(data.error) {
    console.error(data.error);
  }
  else {
    console.log("Stack " + data.stack.name + " has been created for environment " + program.env);
  }
});
Look for more information on my blog. DWP