nsolid-manager
Version:
A manager that sets up and runs N|Solid for you
161 lines (142 loc) • 5 kB
JavaScript
module.exports = Router
const Registry = require('nsolid-registry-etcd')
const debug = require('util').debuglog('nsolid')
const through2 = require('through2')
const Client = require('nsolid-rpcclient')
// TBD: a custom endpoint for users to stuff metrics in, maybe a npm.im/statware
function Router (options) {
if (!(this instanceof Router)) {
return new Router(options)
}
this.registry = Registry(options.etcd_host, {timeout: 5000})
this.denied = options.denied
this.broadcast_approved = options.broadcast_approved
debug('initialized Router: ' + options.etcd_host)
}
Router.prototype.list = function list (req, proxySpec, next) {
debug('list')
let app = proxySpec.app || '*'
let toStream = through2()
this.registry.list(app, function procList (err, hosts) {
if (err) {
toStream.emit('error', err)
return
}
debug('host list from registry: ', hosts)
// Convert the array into a newline-delimited JSON stream
// It is a "fake" stream for now as it is entirely buffered before we stream it
for (let i = 0; i < hosts.length; i++) {
toStream.write(JSON.stringify(hosts[i]) + '\n')
}
toStream.end()
})
return next(null, toStream)
}
Router.prototype.proxy = function proxy (req, proxySpec, next) {
debug('proxy', proxySpec)
let command = proxySpec.command
function asyncError (err) {
setImmediate(function soon () {
next(err)
})
}
if (command == null) {
let err = new TypeError('Missing required command!')
return asyncError(err)
}
if (this.denied.indexOf(command) >= 0) {
let err = new Error('Refusing to send denied command ' + command)
return asyncError(err)
}
let id = proxySpec.id
let app = proxySpec.app
let timeout = proxySpec.timeout
if (id == null) {
if (app == null) {
// switch no app to wildcard for broadcast when no id
app = '*'
}
if (this.broadcast_approved.indexOf(command) < 0) {
let err = new Error('Command \'' + command + '\' is not broadcast approved, id is required.')
return asyncError(err)
}
return this._broadcastLookup(command, app, proxySpec.data, next, timeout)
}
if (app == null) {
let err = new Error('`app` and `id` are both required for lookup')
return asyncError(err)
}
return this._singleProxyLookup(command, app, id, proxySpec.data, next, timeout)
}
Router.prototype._broadcastLookup = function _broadcastLookup (command, app, data, next, timeout) {
let self = this
this.registry.list(app, function procList (err, hosts) {
if (err) {
return next(err)
}
if (hosts.length === 0) {
debug('no hosts found to broadcast to')
return next(null, null)
}
// TBD how does etcd handle 1000s of hosts?
// speed fast enough?
// need to stream?
self._broadcast(hosts, command, data, next, timeout)
})
}
Router.prototype._singleProxyLookup = function _singleProxyLookup (command, app, id, data, next, timeout) {
let self = this
this.registry.lookup(app, id, function (err, host) {
if (err) {
return next(err)
}
if (host == null || host.address == null) {
return next(new Error('Unable to locate host at id ' + id))
}
self._proxyOneHost(host.address, command, data, next, timeout)
})
}
Router.prototype._proxyOneHost = function _proxyOneHost (address, command, data, next, timeout) {
debug('proxying %s to single endpoint %s', command, address)
let req = Client.sendTo(address, command, data, timeout)
next(null, req)
}
Router.prototype._broadcast = function _broadcast (hosts, command, data, next, timeout) {
debug('proxying %s to %s endpoints', command, hosts.length)
let resultStream = through2()
let count = 0
// TBD: this forEach could be problematic for 100s+ of hosts, may need to:
// 1. send in batches
// 2. add a total timeout
hosts.forEach(function proxy (host) {
Client.sendToBuffered(host.address, command, data, function finished (err, response, body) {
debug('handling response for ' + host.address)
// Concatenate in metadata to identify where the reply came from
let pid = host.pid || 0
let prefix = '{"meta":{"id":"' + host.id +
'","app":"' + host.app +
'","pid":' + pid +
',"hostname":"' + host.hostname +
'","address":"' + host.address + '"},'
resultStream.write(prefix)
if (err) {
debug(err)
resultStream.write('"error":')
resultStream.write(JSON.stringify(err.message))
} else if (response.statusCode >= 300) {
debug('agent replied with status code ' + response.statusCode)
resultStream.write('"error":')
resultStream.write(body)
} else {
resultStream.write('"reply":')
resultStream.write(body)
}
resultStream.write('}\n')
if (++count === hosts.length) {
resultStream.end()
}
}, timeout)
})
next(null, resultStream)
}