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
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.