deployment
Version:
Continuous deployment for the masses. Download the latest version of your GitHub package, run tests and deploy to the specified directory. Run a deployment server to launch deployments from the internet, and integrate with GitHub easily. Includes an API t
492 lines (454 loc) • 11.4 kB
JavaScript
;
/**
* Server to listen for deployment intents.
* (C) 2013 Alex Fernández.
*/
// requires
require('prototypes');
var testing = require('testing');
var emailjs = require('emailjs');
var http = require('http');
var util = require('util');
var url = require('url');
var Log = require('log');
var deployment = require('./deployment.js');
var token = require('./token.js');
// globals
var log = new Log('info');
// constants
var PORT = 3470;
var GREEN = '\u001b[32m';
var RED = '\u001b[1;31m';
var BLACK = '\u001b[0m';
/**
* A deployment server, with options.
*/
function DeploymentServer(options)
{
// self-reference
var self = this;
// attributes
var port = options.port || PORT;
if (!options.token)
{
options.token = token.createToken();
log.info('Creating random token: %s', options.token);
}
var server;
options.packageName = options.packageName || 'unnamed';
// init
if (options.quiet)
{
log.level = 'notice';
}
/**
* Start the server.
* An optional callback will be called after the server has started.
*/
self.start = function(callback)
{
server = http.createServer(listen);
server.on('error', function(error)
{
if (error.code == 'EADDRINUSE')
{
return createError('Port ' + port + ' in use, please free it and retry again', callback);
}
return createError('Could not start server on port ' + port + ': ' + error, callback);
});
server.listen(port, function()
{
log.info('Listening on endpoint http://localhost:' + port + '/deploy/' + options.token);
if (callback)
{
callback();
}
});
return server;
};
/**
* Log an error, or send to the callback if present.
*/
function createError(message, callback)
{
if (!callback)
{
return log.error(message);
}
callback(message);
}
/**
* Listen to an incoming request.
*/
function listen(request, response)
{
var parsed = url.parse(request.url, true);
var manual = false;
if (parsed.path == '/deploy/' + options.token + '/manual')
{
manual = true;
}
else if (!parsed.path.startsWith('/deploy/' + options.token))
{
response.statusCode = 403;
response.end('Bad request');
return;
}
var start = Date.now();
request.body = '';
request.on('data', function(data)
{
request.body += data.toString();
});
request.on('end', function()
{
var elapsed = Date.now() - start;
log.info('Request finished in %s ms', elapsed);
var filteredLog = new FilteredLog(log);
if (manual)
{
var pageLog = new WebPageLog('Manual Deployment', filteredLog, response);
deployment.run(options, pageLog, function(error, result)
{
if (error)
{
pageLog.error(error);
}
else if (result)
{
pageLog.notice(result);
}
pageLog.close();
});
}
else
{
response.end('OK');
if (options.emailUser)
{
var emailLog = new EmailLog(options, filteredLog);
deployment.run(options, emailLog, function(error, result)
{
emailLog.sendEmail(error, result, function(error, result)
{
if (error)
{
log.error('Email not sent: %s', error);
}
return log.info('Email sent: %s', result);
});
});
return;
}
deployment.run(options, filteredLog);
}
});
}
}
/**
* A log that filters color codes.
*/
var FilteredLog = function(log)
{
// self-reference
var self = this;
// attributes
self.levels = ['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'];
var forbidden = [GREEN, RED, BLACK];
// init
self.levels.forEach(function(level)
{
self[level] = function()
{
var replaced = util.format.apply(util, arguments);
var filtered = filter(replaced);
log[level].call(log, filtered);
return filtered;
};
});
/**
* Filter a message.
*/
function filter(message)
{
forbidden.forEach(function(string)
{
message = message.replaceAll(string, '');
});
return message;
}
};
/**
* Test the filtered log.
*/
function testFilteredLog(callback)
{
var filteredLog = new FilteredLog(log);
testing.assertEquals(filteredLog.info(RED + 'Here' + GREEN), 'Here', 'Should filter colors out', callback);
testing.success(callback);
}
/**
* A log object that writes to the response for a web page.
*/
var WebPageLog = function(title, log, response)
{
// self-reference
var self = this;
// attributes
self.levels = {
'debug': 'color: grey',
'info': 'color: black',
'notice': 'color: black; font-weight: bold',
'warning': 'color: orange',
'error': 'color: red',
'critical': 'color: red; font-style: italic',
'alert': 'color: red; font-weight: bold',
'emergency': 'color: red; font-weight: bold; font-style: italic',
};
var replacements = {
'\n': '<br>\n',
};
replacements[GREEN] = '<span style="color: green">';
replacements[RED] = '<span style="color: red">';
replacements[BLACK] = '</span>';
// init
response.writeHead(200, 'OK', {
'Content-Type': 'text/html',
});
response.write('<!DOCTYPE html>\n<html>\n<head><meta charset="UTF-8"><title>');
response.write(title);
response.write('</title></head>\n<body>\n');
response.write('<h1>Deployment Launched</h1>\n');
for (var level in self.levels)
{
self[level] = getShow(level, self.levels[level]);
}
/**
* Get a function to show a message for a log level, in the given style.
*/
function getShow(level, style)
{
return function()
{
// first, to original log
var fn = log[level];
fn.apply(log, arguments);
// now show in web page
var replaced = util.format.apply(util, arguments);
for (var original in replacements)
{
replaced = replaced.replaceAll(original, replacements[original]);
}
response.write('<p style="' + style + '">\n' + replaced + '\n</p>\n');
};
}
/**
* Finish the web page.
*/
self.close = function()
{
response.end('</body>\n</html>');
};
};
/**
* A response that stores every message and returns them as a web page.
*/
var WebPageResponse = function()
{
// self-reference
var self = this;
// attributes
self.statusCode = 0;
self.result = '';
self.finished = false;
/**
* Write the head first.
*/
self.writeHead = function(code)
{
self.statusCode = code;
};
/**
* Write any message.
*/
self.write = function(message)
{
self.result += message;
};
/**
* End the web page.
*/
self.end = function(message)
{
self.result += message;
self.finished = true;
};
};
/**
* Test the web page log.
*/
function testWebPageLog(callback)
{
var response = new WebPageResponse();
var webPage = new WebPageLog('Test', log, response);
testing.assertEquals(response.statusCode, 200, 'Invalid status code', callback);
webPage.info('Here');
webPage.error('There');
testing.assert(!response.finished, 'Should not have finished the page yet', callback);
webPage.close();
testing.assert(response.finished, 'Should have finished the page by now', callback);
testing.assert(response.result.contains('Here'), 'Should contain first message', callback);
testing.assert(response.result.contains('There'), 'Should contain second message', callback);
testing.success(callback);
}
/**
* A log that stores everything in a web page, then sends an email.
*/
var EmailLog = function(options, log)
{
// self-reference
var self = this;
// attributes
var response = new WebPageResponse();
var pageLog = new WebPageLog('Automatic Deployment', log, response);
var mailServer = emailjs.server.connect({
user: options.emailUser,
password: options.emailPassword,
host: options.emailHost,
ssl: (options.emailSsl == 'true'),
});
// init
log.debug('Connected to mail server %s', options.emailHost);
for (var level in pageLog.levels)
{
self[level] = getShow(level);
}
/**
* Get a function to show the given level.
*/
function getShow(level)
{
return function()
{
// send to original log
var fn = pageLog[level];
fn.apply(pageLog, arguments);
};
}
/**
* Send an email with the result of the deployment.
*/
self.sendEmail = function(error, result, callback)
{
var params = {
from: options.emailFrom,
to: options.emailTo,
text: 'Package ' + options.packageName + '\n',
};
if (error)
{
params.subject = 'Deployment for ' + options.packageName + ' failed';
params.text += 'Deployment failed: ' + error;
pageLog.alert(params.text);
}
else
{
params.subject = 'Deployment for ' + options.packageName + ' successful';
params.text += 'Deployment successful: ' + result;
pageLog.notice(params.text);
}
pageLog.close();
var attachments = [ {
data: response.result,
alternative: true,
}];
params.attachment = attachments;
mailServer.send(params, function(error)
{
if (error)
{
return callback('Email not sent: ' + error);
}
return callback(null, '"' + params.subject + '" sent to ' + params.to);
});
};
};
/**
* Tests sending email. For a functional test you have to supply your own credentaials:
* emailHost, emailUser, emailPassword, emailSsl (true or false), emailFrom, emailTo.
* Place them in a JSON file called .email-credentails.json in the root of the package.
*/
function testEmailLog(callback)
{
var credentials;
try
{
credentials = require('../.email-credentials.json');
}
catch(exception)
{
log.info('Not testing email: %s', exception);
return callback(null);
}
credentials.packageName = "test";
var emailLog = new EmailLog(credentials, log);
emailLog.sendEmail(null, 'Testing', function(error, sent)
{
testing.check(error, 'Could not send mail', callback);
testing.assert(sent, 'Did not send mail', callback);
testing.success(callback);
});
}
/**
* Start a deployment server. Options can contain:
* - port: Port to use, default 3470.
* - token: Unique, random string needed in the path, for security reasons.
* See the docs for details.
* - packageName: Name of the package for messages.
* - quiet: Do not log any messages.
* - directory: Directory where the package currently resides.
* - testDirectory: Directory where the test version of the package resides.
* - deploymentCommand: a command to run after a successful deployment,
* e.g. "sudo restart myService".
* - timeout: Seconds to wait for all commands including tests, default 10 seconds.
* - detail: If true, show full log and diff of code to deploy.
* - emailUser: User for email server');
* - emailPassword: Password for email server');
* - emailHost: Host for email server');
* - emailSsl: "true" to enable SSL');
* - emailFrom: Email address that generates the message');
* - emailTo: Destination for deployment message');
*
* An optional callback is called after the server has started.
* In this case the quiet option is enabled.
*/
exports.startServer = function(options, callback)
{
if (callback)
{
options.quiet = true;
}
if (!options.testDirectory && !options.directory)
{
log.warning('No directories given; no deployment will be done');
}
var server = new DeploymentServer(options);
return server.start(callback);
};
/**
* Run all tests.
*/
exports.test = function(callback)
{
testing.run([
testEmailLog,
testWebPageLog,
testFilteredLog,
], 10000, callback);
};
// run tests if invoked directly
if (__filename == process.argv[1])
{
log = new Log('debug');
exports.test(testing.show);
}