Adding a brain to Hubot

In Hacking Hubot - Intro I walked through my plan to teach Hubot how to push builds to Azure.

@hubot build deploy nicks-dev

Voila! Hubot would connect to Kudu's api and a build would be started. Before we implemented that command we wanted to be able to set aliases for environments. Two reasons for the need to set aliases: we didn't want people to have to know the "azure" site that they were deploying and we wanted qa to be able to just type

hubot build deploy qa|test|production|etc

The other command we wanted to write was so we could control what sites could be deployed. This is also known as poor man's security.

So we wanted to write another command for developers to use to configure new deployable sites:

@hubot build set [site] [alias]

Where shall we start?

Hubot's Brain

Dilbert Brain

Hubot comes with an out-of-the-box, in-memory, simplistic brain. Source can be found here, its really easy to read and you will see some of the methods that are available. Sounds perfect for the task at hand right? Well, the "in-memory" tells us that upon server restart all of our knowledge will be reset. Redis and redis-brain to the rescue. We are going to use a more durable in-memory data structure store Redis and redis-brain which extends Hubot's brain to save to Redis. Just a quick note about Redis. Redis advertises itself as "in-memory" but it also will persist our data to disk as well.

Redis Setup

On my mac I installed Redis via the method explained here. For our production Heroku node we use the free Redis to go add-in.

Lets save some data

Hubot makes saving data simple. When you define your handler add a parameter called robot. Robot has a bunch of methods that are documented in the source: robot.coffee. We are most interested in the robot.brain property.

robot.brain.get(key) retrieves a value from brain.
robot.brain.set(key, value) will save something to brain.

How we ended up using it:

module.exports = function(robot) {  
  var self = this;
  self.handle_set = handle_set;
  //our only respond listener
  robot.respond(/build\s+(.*)/i, onRequestForBuild);

  // this handles the build set command.
  function handle_set(req, options){
    //1. retrieve the mappings from brain 
    var aliasMappings = robot
                          .brain
                          .get('aliasMappings') || {};
    //do magical stuff here to change things
    magic();
    //2. save them back to brain
    robot.brain.set('aliasMappings', aliasMappings);
  }

  function onRequestForBuild(res){
    var options = _parseArg(res.match[1]);
    var func = self['handle_'+ options.command];
    if(func){
        return func(res, options);
    }
  }
}

Above was our templated pattern for how we handle all calls to @hubot [build] [command] [action] [args args ...]. Our command this time was set so we implemented the handle_set command to detail with processing the request and responding.
The full handle_set command.

  // `hubot build set <server name> <alias>
  function handle_set(req, options){
    var serverName = options.arguments[0],
        alias = options.arguments.slice(1).join('_');

    if (serverName && alias) {

      var aliasMappings = robot.brain
                               .get('aliasMappings') || {};
      //only add an alias if force is specified
      // yes this is poor mans security
      if(!aliasMappings[serverName] && 
           options.action !== 'force'){
          req.send('You are not allowed to add an alias for:' + serverName);
        return ;
      }
      //default if null
      aliasMappings[serverName] = 
             aliasMappings[serverName] ||  [];
      //make sure the mapping doesn't already exist
      if (!_aliasLookup(alias)) {
        aliasMappings[serverName].push(alias);
        robot.brain.set('aliasMappings', aliasMappings);
        req.send('set alias, ' + 
                  alias + 
                 ', for server, ' + 
                 serverName);
      } else {
        //reply to user that the alias already exists.
        req.send('that alias exists, pick another name:', 
                 aliasMappings[serverName].join(', '));
      }
    }
    else {
      req.send('invalid arguments.');
    }

  }

Not to painful right? We now have the ability to save aliases. Stay tuned, next week I will walk through how to send commands to Kudu.

Part 3: Kudu how I loath the.

Cheers.

Nick Capito

Professional developer in all things generic. Good at AngularJS, CSS3 , Sass, .NET, Azure, Node. Love dogs. In an alternate live I would have been an American Cesar Millan.

Richmond, VA http://nixo.us