thalassa-aqueduct
Version:
Dynamic haproxy load balancer and configuration. Part of Thalassa
213 lines (177 loc) • 7.11 kB
JavaScript
var handlebars = require('handlebars')
, HAProxy = require('haproxy')
, fs = require('fs')
, resolve = require('path').resolve
, util = require('util')
, f = util.format
, assert = require('assert')
, EventEmitter = require('events').EventEmitter
, debounce = require('debounce')
, deepEqual = require('deep-equal')
;
var HAProxyManager = module.exports = function HAProxyManager (opts) {
if (typeof opts !== 'object') opts = {};
assert(opts.data, 'opts.data required');
assert(opts.haproxy, 'opts.haproxy required');
this.config = {};
this.config.templateFile = resolve(opts.templateFile || __dirname + '/../default.haproxycfg.tmpl');
this.config.haproxyCfgPath = resolve(opts.haproxyCfgPath || '/etc/haproxy/haproxy.cfg');
this.config.watchConfigFile = (opts.watchConfigFile !== undefined) ? opts.watchConfigFile : true;
this.config.debounceRate = opts.debounceRate || 2000;
this.log = (typeof opts.log === 'function') ? opts.log : function (){};
this.latestConfig = "";
if (!fs.existsSync(this.config.templateFile)) {
this.log('error', f("template file %s doesn't exists!", this.config.templateFile));
}
this.template = handlebars.compile(fs.readFileSync(this.config.templateFile, 'utf-8'));
this.writeConfigDebounced = debounce(this.writeConfig.bind(this), this.config.debounceRate, false);
this.data = opts.data;
this.haproxy = opts.haproxy;
this.data.frontends.on( 'changes', this._changeFrontEnd.bind(this) );
this.data.backends.on ( 'changes', this._changeBackEnd.bind(this) );
this.writeConfigDebounced();
};
util.inherits(HAProxyManager, EventEmitter);
HAProxyManager.prototype.writeConfig = function() {
var data = {
frontends: this.data.frontends.toJSON(),
backends: this.data.backends.toJSON(),
haproxySocketPath: this.haproxy.socket
};
var previousConfig = this.latestConfig;
this.latestConfig = this.template(data);
// only write the config and reload if it actually changed
if (!deepEqual(previousConfig, this.latestConfig)) {
fs.writeFileSync(this.config.haproxyCfgPath, this.latestConfig , 'utf-8');
this.emit('configChanged');
this.reload();
}
};
HAProxyManager.prototype.reload = function () {
var self = this;
self.haproxy.running(function (err, running) {
if (err) return self.log('error', 'HaproxyManager.reload', { error: String(err) });
function handleRestart (err) {
if (err) return self.log('error', 'HaproxyManager.reload', { error: String(err) });
}
if (running) self.haproxy.reload(handleRestart);
else self.haproxy.start(handleRestart);
self.emit('reloaded');
});
};
HAProxyManager.prototype._changeFrontEnd = function(row, changed) {
this.log('debug', 'HaproxyManager._changeFrontEnd', changed);
this.writeConfigDebounced();
};
HAProxyManager.prototype._changeBackEnd = function(row, changed) {
this.log('debug', 'HaproxyManager_changeBackEnd', changed);
this.writeConfigDebounced();
};
//
//
//
//
//
//
//
// TODO refactor all these helper, reconsider business logic
//
// template helper for outputing FrontEnd acl rules
handlebars.registerHelper('aclRule', function (rule) {
var rand = Math.random().toString(36).substring(3);
var name = rule.type + '_' + rand;
if (rule.type === 'path' || rule.type === 'url') {
return util.format("acl %s %s %s\nuse_backend %s if %s\n", name, rule.operation, rule.value, rule.backend, name);
}
else if (rule.type === 'header') {
return util.format("acl %s %s(%s) %s\nuse_backend %s if %s\n", name, rule.operation, rule.header, rule.value, rule.backend, name);
}
});
handlebars.registerHelper('frontendHelper', function (frontend) {
var output = [];
var hasRules = frontend.rules && frontend.rules.length > 0;
var hasNatives = frontend.natives && frontend.natives.length > 0;
output.push("bind " + frontend.bind);
output.push("mode " + frontend.mode);
output.push("default_backend " + frontend.backend);
// http only default options
if (frontend.mode === 'http') {
output.push("option httplog");
// The default keep-alive behavior is to use keep-alive if clients and
// backends support it. However, if haproxy will only process rules when
// a connection is first established so if any rules are used then server-close
// should be specified at least and haproxy will let clients use keep-alive
// to haproxy but close the backend connections each time.
//
// If there are any rules, the default behavior is to use http-server-close
// and http-pretend-keepalive
if (frontend.keepalive === 'server-close') {
output.push("option http-server-close");
output.push("option http-pretend-keepalive");
}
else if (frontend.keepalive === 'close'){
output.push("option forceclose");
}
// the default if there are rules is to use server close
else if (hasRules) {
output.push("option http-server-close");
output.push("option http-pretend-keepalive");
}
}
if (hasRules) {
frontend.rules.forEach(function (rule) {
var rand = Math.random().toString(36).substring(13);
var name = rule.type + '_' + rand;
if (rule.type === 'path' || rule.type === 'url') {
output.push(util.format("acl %s %s %s\nuse_backend %s if %s",
name, rule.operation, rule.value, rule.backend, name));
}
else if (rule.type === 'header') {
output.push(util.format("acl %s %s(%s) %s\nuse_backend %s if %s",
name, rule.operation, rule.header, rule.value, rule.backend, name));
}
});
}
if (hasNatives) {
frontend.natives.forEach(function (native) {
output.push(native);
});
}
return output.join('\n ');
});
// helper to output http check and servers block
handlebars.registerHelper('backendHelper', function (backend) {
var host = backend.host;
var health = backend.health;
var members = backend.members;
var output = [];
var hasNatives = backend.natives && backend.natives.length > 0;
// output mode and balance options
output.push("mode " + backend.mode);
output.push("balance " + backend.balance);
// host header propagation
if (backend.host) {
output.push("reqirep ^Host:\\ .* Host:\\ " + backend.host);
}
// option httpchk
if (backend.mode === 'http' && health) {
var httpVersion = (health.httpVersion === 'HTTP/1.1') ?
('HTTP/1.1\\r\\nHost:\\ ' + backend.host) :
health.httpVersion;
output.push(util.format("option httpchk %s %s %s", health.method, health.uri, httpVersion));
}
if (hasNatives) {
backend.natives.forEach(function (native) {
output.push(native);
});
}
if (members) {
// server lines for each member
members.forEach(function (member) {
var name = util.format("%s_%s:%s", backend.key, member.host, member.port);
var interval = (health) ? health.interval : 2000;
output.push(util.format("server %s %s:%s check inter %s", name, member.host, member.port, interval));
});
}
return output.join('\n ');
});