node-swarm-kb
Version:
Kubernetes based inter-cluster communication
217 lines (202 loc) • 6.25 kB
JavaScript
const DEFAULTOPTIONS = { };
let Kubernetes = require('./kubernetes'); // let because it may be overloaded if 'standalone' is set ti true.
const Messages = require('./messages');
const Tasks = require('./tasks');
const localIps = require('./localIps');
const Promise = require('bluebird');
const appPackage = require('../package.json');
const debug = require('debug')(appPackage.name);
debug(`Loading.. localIps: ${localIps}`);
/**
* Module class
*
* @param {object} options
**/
function init (options) {
if (options && typeof options === 'object') {
Object.keys(this._handlers).forEach(eventName =>
(onEventName =>
options[onEventName] && this._handlers[eventName].indexOf() === -1 && this._handlers[eventName].push(options[onEventName])
)(`on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`));
}
if (this._inited) {
return Promise.resolve(this.nodes);
}
this._inited = true;
Object.assign(this.options, options);
let _resolve;
const promise = new Promise((resolve, reject) => {
_resolve = resolve;
});
if (this.options.standalone) {
Kubernetes = require('./standalone');
}
/* Start node management */
this._kubernetes = new Kubernetes(Object.assign(this.options, {
onNodeAdded: node =>
localIps.indexOf(node.ip) === -1 && (
debug(`New Ip ${node.ip} added to cluster!`) ||
module.exports.emit('nodeAdded', node)
),
onNodeRemoved: node =>
localIps.indexOf(node.ip) === -1
? (
debug(`Ip ${node.ip} removed from cluster!`) ||
module.exports.emit('nodeRemoved', node)
) : (
debug(`Local Ip ${node.ip} removed from cluster, triggering SIGTERM!`) ||
process.emit('SIGTERM')
),
onError: error =>
debug(`[Error] kubernetes Error: ${JSON.stringify(error)}!`) ||
module.exports.emit('error', error),
onReady: nodes => {
debug(`Kubernetes ready with ${Object.keys(nodes).length} nodes.`);
// elect the cluster master
this._election();
// When the master is removed, elect a new master
this.on('nodeRemoved', node =>
node.ip === this.master && this._election());
if (!this.master) {
// If not nodes in cluster select first node as master
this.once('nodeAdded', this._election.bind(this));
}
module.exports.isReady = true;
_resolve(nodes);
return module.exports.emit('ready', nodes);
}
}));
/* Start and link the inter-node communication */
if (this.options.standalone) {
this.messages = this._kubernetes;
} else {
this.messages = new Messages(Object.assign(this.options.messages || {}, {
onError: error =>
debug(`[Error] messages Error: ${error.message}!`) ||
module.exports.emit('error', error)
}));
}
// Add the tasks processing module
this.tasks = new Tasks(this, this.options.tasks);
return promise;
}
module.exports = {
isReady: false,
_inited: false,
init,
/* Set Accessors to Kubernetes nodes : key = ip, value = info object */
get nodes () {
return this._kubernetes && this._kubernetes.nodes || {};
},
options: Object.assign({}, DEFAULTOPTIONS),
_handlers: {
ready: [],
error: [],
nodeAdded: [],
nodeRemoved: []
},
_handlersOnce: {
ready: [],
error: [],
nodeAdded: [],
nodeRemoved: []
},
/**
* Add a listener to one event
* @param {string} event - event name to listen on
* @param {function} handler - function to execute
**/
on (event, handler) {
if (this._handlers[event].indexOf(handler) === -1) {
this._handlers[event].push(handler);
}
},
/**
* Add a listener to one event & remove it once emitted
* @param {string} event - event name to listen on
* @param {function} handler - function to execute
**/
once (event, handler) {
if (this._handlersOnce[event].indexOf(handler) === -1) {
this._handlersOnce[event].push(handler);
}
},
/**
* Trigger listerners
* @param {string} event - event name to trigger
* @param {any[]} args - arguments
**/
emit (event, args) {
for (const handler of (this._handlers[event] || []).concat(this._handlersOnce[event])) {
if (args instanceof Array) {
handler(...args);
} else {
handler(args);
}
}
this._handlersOnce[event] = [];
},
/**
* Send a message to a node of the cluster
* @param {string|object} node - destination node
* @param {string|string[]} topic - message topic
* @param {string|object} message - data to send
* @returns {promise}
**/
send (node, topic, message) {
let error;
if (!topic || !topic.length) {
error = new Error('Unable to send message: Missing topic');
}
if (!node) {
error = new Error('Unable to send message: Missing node');
}
if (error) {
debug(`[Error] ${error.message}!`);
this.emit('error', error);
return Promise.reject(error);
}
if (message === undefined) {
return this.broadcast(node, topic);
}
// dont send message to yourself
if (localIps.indexOf(node.ip || node) !== -1) {
return Promise.resolve();
}
return this.messages.send(node.ip || node, topic, message);
},
/**
* Send a message to all nodes of the cluster
* @param {string|string[]} topic - message topic
* @param {string|object} message - to send
* @returns {promise}
**/
broadcast (topic, message) {
return Promise.all(
Object.keys(module.exports._kubernetes.nodes).map(ip => this.send(ip, topic, message))
);
},
// function regarding cluster master
master: null,
_election () {
// for now a simple election, the smallest ip wins
this.master = Object.keys(this.nodes).sort()[0];
debug(`New Master elected: ${this.master}`);
return this.master;
},
/**
* Indicate if this server is the master of the cluster
* @returns {boolean}
**/
get amIMaster () {
return localIps.indexOf(this.master) !== -1;
},
/**
* Indicate if this server is the given node
* @param {object<node>|string} node - the node or ip to test
* @returns {boolean}
**/
isMe (node) {
return node && localIps.indexOf(node.ip || node) !== -1;
}
};