UNPKG

tribune

Version:

Holy advocate to the Roman Senate, your Tribune will ensure the Consuls have your interests in mind.

258 lines (207 loc) 7.97 kB
const url = require('url'); const async = require('async'); const TagArray = require('./tag-array'); const Service = require('./service'); const Consul = require('./consul'); const Endpoint = require('./endpoint'); class Tribune { constructor(opts = {}) { if (!opts.agentUrl) { throw new Error('opts.agentUrl is required'); } const agentUrlComponents = url.parse(opts.agentUrl); if (!agentUrlComponents.port) { agentUrlComponents.port = 80; } if (typeof agentUrlComponents.port === 'string') { agentUrlComponents.port = parseFloat(agentUrlComponents.port); } if (!agentUrlComponents.hostname) { throw new Error('a hostname is required in opts.agentUrl'); } if (!agentUrlComponents.protocol) { throw new Error('a protocol is required in opts.agentUrl'); } this.tags = new TagArray(); this._consul = new Consul(opts); this.url = null; this._agentUrlComponents = agentUrlComponents; this._serviceTimeout = opts.serviceTimeout || 10000; this._serviceTtl = opts.serviceTtl || 1800; this._aclToken = opts.aclToken; this._interval = null; this._healthCheckUrl = null; this._urlComponents = null; this._serviceName = null; this._serviceId = ''; this._preparedQueryId = ''; } get isRegistered() { return !!this._serviceId && !!this._preparedQueryId; } register(serviceName, opts = {}, cb = () => {}) { if (this.isRegistered) { return this.deregister((err) => { if (err) { return cb(err); } this.register(serviceName, opts, cb); }); } if (!serviceName) { return cb(new Error('serviceName is required')); } if (!opts.url) { return cb(new Error('opts.url is required')); } if (!opts.healthCheckUrl) { return cb(new Error('opts.healthCheckUrl is required')); } const urlComponents = url.parse(opts.url); if (!urlComponents.port) { urlComponents.port = 80; } if (typeof urlComponents.port === 'string') { urlComponents.port = parseFloat(urlComponents.port); } if (!urlComponents.hostname) { return cb(new Error('a hostname is required in opts.url')); } if (!urlComponents.protocol) { return cb(new Error('a protocol is required in opts.url')); } if (opts.healthCheckUrl[0] === '/') { opts.healthCheckUrl = opts.healthCheckUrl.slice(1); } this.url = opts.url; this._serviceName = serviceName; this._urlComponents = urlComponents; this._healthCheckUrl = opts.healthCheckUrl; this._interval = opts.interval || 1000; this.tags.set('protocol', this._urlComponents.protocol); if (opts.routes) { this.tags.set('routes', opts.routes.toString()); } async.series([ (cb) => this._deregisterPreparedQueryDupe(cb), (cb) => this._deregisterServiceDupes(cb), (cb) => this._deregisterServiceGhosts(cb) ], (err) => { if (err) { return cb(err); } this._generateServiceId(); async.parallel([ (cb) => this._consul.service.register(this._generateServiceBody(), cb), (cb) => this._consul.preparedQuery.register(this._generatePreparedQueryBody(), cb) ], (err, res) => { if (err) { return cb(err); } this._preparedQueryId = res[1]; cb(null); }); }); } deregister(cb = () => {}) { if (!this.isRegistered ) { return cb(new Error('Cannot deregister. Register not called.')); } async.series([ (cb) => this._consul.service.deregister(this._serviceId, cb), (cb) => this._deregisterPreparedQueryGhost(cb) ], (err) => { if (err) { return cb(err); } this._serviceId = ''; this._preparedQueryId = ''; cb(null); }); } service(serviceName) { return new Service(this, serviceName); } endpoints(query = {}, cb = () => {}) { const notIn = query.notIn || []; notIn.push('consul'); this._consul.service.get((err, services) => { if (err) { return cb(err); } const endpoints = Object.keys(services) .map(key => new Endpoint(services[key])) .filter(endpoint => notIn.indexOf(endpoint.name) === -1); cb(null, endpoints); }); } status(opts = {}, cb = () => {}) { const minPeers = opts.minPeers || 3; async.parallel([ (cb) => this._consul.getLeader(cb), (cb) => this._consul.getPeers(cb) ], (err, res) => { if (err) { return cb(err); } const leader = res[0]; const peers = res[1]; if (!leader) { return cb(null, { statusCode: 503, message: 'Consul Leader status failed' }); } if (!peers || peers.length < minPeers) { return cb(null, { statusCode: 503, message:'Peers status failed' }); } cb(null, { statusCode: 200, message: 'OK' }); }); } _deregisterPreparedQueryGhost(cb = () => {}) { this._consul.preparedQuery.execute(this._serviceName, undefined, (err, res) => { if (err) { return cb(err); } if (!!res.Nodes.length) { return cb(null); } this._consul.preparedQuery.deregister(this._preparedQueryId, cb); }); } _deregisterPreparedQueryDupe(cb = () => {}) { this._consul.preparedQuery.get((err, preparedQueries) => { if (err) { return cb(err); } if (!preparedQueries.length) { return cb(null); } const pq = preparedQueries.find(x => x.Name === this._serviceName); if (!pq) { return cb(null); } this._consul.preparedQuery.deregister(pq.ID, cb); }); } _deregisterServiceDupes(cb = () => {}) { this._consul.service.get((err, res) => { if (err) { return cb(err); } const { hostname, port } = this._urlComponents; const serviceDupeIds = Object.keys(res) .map((serviceId) => res[serviceId]) .filter((service) => service.Address === hostname && service.Port === port) .map((service) => service.ID); async.each(serviceDupeIds, (serviceDupeId, cb) => { this._consul.service.deregister(serviceDupeId, cb); }, cb); }); } _deregisterServiceGhosts(cb = () => {}) { this._consul.service.checks((err, res) => { if (err) { return cb(err); } const serviceGhostIds = Object.keys(res) .map((checkId) => res[checkId]) .filter((check) => check.Status === 'critical' && check.ServiceName === this._serviceName) .map((check) => check.ServiceID); async.each(serviceGhostIds, (serviceGhostId, cb) => { this._consul.service.deregister(serviceGhostId, cb); }, cb); }); } _getConsulServiceEndpoint(serviceName, cb = () => {}) { this._consul.preparedQuery.execute(serviceName, 1, (err, res) => { if (err) { return cb(err); } if (!res) { return cb(null, null); } cb(null, res.Nodes[0].Service); }); } _generateServiceBody() { return { ID : this._serviceId, Name : this._serviceName, Tags : this.tags, Address: this._urlComponents.hostname, Port : parseFloat(this._urlComponents.port), Check: { HTTP : `${this._healthCheckUrl}`, Interval: `${this._interval}ms` } }; } _generatePreparedQueryBody() { return { Name : this._serviceName, Service: { Service: this._serviceName } }; } _generateServiceId() { const pad = (str, len) => '0'.repeat(len - str.length) + str.slice(0, len); const date = new Date(); const year = pad(date.getFullYear().toString(), 4); const month = pad((date.getMonth() + 1).toString(), 2); const day = pad((date.getDate()).toString(), 2); const rand = pad(Math.floor(Math.random() * 1000).toString(), 4); this._serviceId = `${this._serviceName}-${year}${month}${day}${rand}`; } } module.exports = Tribune;