UNPKG

guvnor

Version:

A node process manager that isn't spanners all the way down

892 lines (724 loc) 24.8 kB
var expect = require('chai').expect, sinon = require('sinon'), RemoteRPC = require('../../../../lib/daemon/rpc/RemoteRPC'), WildEmitter = require('wildemitter'), EventEmitter = require('events').EventEmitter describe('RemoteRPC', function () { var remoteRpc beforeEach(function () { remoteRpc = new RemoteRPC() remoteRpc._logger = { info: sinon.stub(), warn: sinon.stub(), error: sinon.stub(), debug: sinon.stub() } remoteRpc._guvnor = { on: sinon.stub(), findProcessInfoById: sinon.stub(), findProcessInfoByName: sinon.stub(), findProcessInfoByPid: sinon.stub(), findAppById: sinon.stub(), findAppByName: sinon.stub(), getServerStatus: sinon.stub(), listProcesses: sinon.stub(), listApplications: sinon.stub(), deployApplication: sinon.stub(), removeApplication: sinon.stub(), switchApplicationRef: sinon.stub(), listApplicationRefs: sinon.stub(), updateApplicationRefs: sinon.stub(), startProcess: sinon.stub(), removeProcess: sinon.stub(), currentRef: sinon.stub(), listUsers: sinon.stub(), stopProcess: sinon.stub() } remoteRpc._processService = { on: sinon.stub() } remoteRpc._appService = { on: sinon.stub() } remoteRpc._dnode = sinon.stub() remoteRpc._config = { remote: { enabled: true }, guvnor: { rpctimeout: 0 } } remoteRpc._remoteUserService = { findUser: sinon.stub() } remoteRpc._crypto = { verify: sinon.stub() } remoteRpc._processFactory = {} remoteRpc._child_process = { fork: sinon.stub(), exec: sinon.stub() } remoteRpc._package = {} remoteRpc._os = { hostname: sinon.stub(), type: sinon.stub(), platform: sinon.stub(), arch: sinon.stub(), release: sinon.stub() } remoteRpc._pem = { createCertificate: sinon.stub() } remoteRpc._tls = { createServer: sinon.stub() } remoteRpc._fs = { readFile: sinon.stub() } remoteRpc._mdns = { createAdvertisement: sinon.stub(), tcp: sinon.stub() } remoteRpc._userDetailsStore = { findOrCreate: sinon.stub() } remoteRpc._fileSystem = { getRunDir: sinon.stub() } }) it('should not start dnode if remote is not enabled', function (done) { remoteRpc._config.remote.enabled = false remoteRpc.afterPropertiesSet(function () { expect(remoteRpc._dnode.called).to.be.false done() }) }) it('should start dnode', function (done) { remoteRpc._config.remote.port = 8080 remoteRpc._config.remote.host = 'foo' remoteRpc._pem.createCertificate.callsArgWith(1, undefined, { serviceKey: 'key', certificate: 'certificate' }) var server = { listen: sinon.stub(), on: sinon.stub() } server.listen.withArgs( remoteRpc._config.remote.port, remoteRpc._config.remote.host, sinon.match.func ).callsArgWith(2, undefined) remoteRpc._tls.createServer.returns(server) remoteRpc.afterPropertiesSet(function (error) { expect(error).to.not.exist expect(remoteRpc.port).to.equal(remoteRpc._config.remote.port) done() }) }) it('should start dnode with user defined certificates', function (done) { remoteRpc._config.remote.port = 8080 remoteRpc._config.remote.host = 'foo' remoteRpc._config.remote.key = 'keyfile' remoteRpc._config.remote.certificate = 'certfile' remoteRpc._fs.readFile.withArgs(remoteRpc._config.remote.key, sinon.match.func).callsArgWith(1, undefined, 'key') remoteRpc._fs.readFile.withArgs(remoteRpc._config.remote.certificate, sinon.match.func).callsArgWith(1, undefined, 'cert') var server = { listen: sinon.stub(), on: sinon.stub() } server.listen.withArgs( remoteRpc._config.remote.port, remoteRpc._config.remote.host, sinon.match.func ).callsArgWith(2, undefined) remoteRpc._tls.createServer.returns(server) remoteRpc.afterPropertiesSet(function (error) { expect(error).to.not.exist expect(remoteRpc.port).to.equal(remoteRpc._config.remote.port) expect(remoteRpc._pem.createCertificate.called).to.be.false // should have used use key expect(remoteRpc._tls.createServer.getCall(0).args[0].key).to.equal('key') expect(remoteRpc._tls.createServer.getCall(0).args[0].cert).to.equal('cert') done() }) }) it('should broadcast events from guvnor and the process manager', function (done) { remoteRpc._guvnor = new WildEmitter() remoteRpc._processService = new WildEmitter() remoteRpc._pem.createCertificate.callsArgWith(1, undefined, { serviceKey: 'key', certificate: 'certificate' }) var server = { listen: sinon.stub(), on: sinon.stub() } server.listen.withArgs( remoteRpc._config.remote.port, remoteRpc._config.remote.host, sinon.match.func ).callsArgWith(2, undefined) remoteRpc._tls.createServer.returns(server) remoteRpc.afterPropertiesSet(function (error) { expect(error).to.not.exist var events = 0 remoteRpc._connections = [{ sendEvent: function () { events++ if (events == 2) { done() } } }] remoteRpc._guvnor.emit('foo') remoteRpc._processService.emit('bar') }) }) it('should store a client connection', function (done) { var d = { pipe: sinon.stub(), on: sinon.stub() } d.pipe.returnsArg(0) remoteRpc._dnode.returns(d) remoteRpc._pem.createCertificate.callsArgWith(1, undefined, { serviceKey: 'key', certificate: 'certificate' }) var server = { listen: sinon.stub(), on: sinon.stub() } server.listen.withArgs( remoteRpc._config.remote.port, remoteRpc._config.remote.host, sinon.match.func ).callsArgWith(2, undefined) var stream = new EventEmitter() stream.pipe = sinon.stub() stream.pipe.returnsArg(0) remoteRpc._tls.createServer.callsArgWith(1, stream).returns(server) remoteRpc.afterPropertiesSet(function (error) { expect(error).to.not.exist var client = {} var connection = new EventEmitter() connection.stream = new EventEmitter() remoteRpc._dnode.getCall(0).args[0](client, connection) expect(remoteRpc._connections[d._id]).to.equal(client) done() }) }) it('should remove a client connection when the connection ends', function (done) { var d = { pipe: sinon.stub(), on: sinon.stub() } d.pipe.returnsArg(0) remoteRpc._dnode.returns(d) remoteRpc._pem.createCertificate.callsArgWith(1, undefined, { serviceKey: 'key', certificate: 'certificate' }) var server = { listen: sinon.stub(), on: sinon.stub() } server.listen.withArgs( remoteRpc._config.remote.port, remoteRpc._config.remote.host, sinon.match.func ).callsArgWith(2, undefined) var stream = new EventEmitter() stream.pipe = sinon.stub() stream.pipe.returnsArg(0) remoteRpc._tls.createServer.callsArgWith(1, stream).returns(server) remoteRpc.afterPropertiesSet(function (error) { expect(error).to.not.exist var client = {} var connection = new EventEmitter() connection.id = 'foo' connection.stream = new EventEmitter() remoteRpc._dnode.getCall(0).args[0](client, connection) expect(remoteRpc._connections[d._id]).to.equal(client) connection.emit('end') expect(remoteRpc._connections[d._id]).to.not.exist done() }) }) it('should remove a client connection when the connection errors', function (done) { var d = { pipe: sinon.stub(), on: sinon.stub(), destroy: sinon.stub() } d.pipe.returnsArg(0) remoteRpc._dnode.returns(d) remoteRpc._pem.createCertificate.callsArgWith(1, undefined, { serviceKey: 'key', certificate: 'certificate' }) var server = { listen: sinon.stub(), on: sinon.stub() } server.listen.withArgs( remoteRpc._config.remote.port, remoteRpc._config.remote.host, sinon.match.func ).callsArgWith(2, undefined) var stream = new EventEmitter() stream.pipe = sinon.stub() stream.pipe.returnsArg(0) stream.destroy = sinon.stub() remoteRpc._tls.createServer.callsArgWith(1, stream).returns(server) remoteRpc.afterPropertiesSet(function (error) { expect(error).to.not.exist var client = {} var connection = new EventEmitter() connection.id = 'foo' connection.stream = new EventEmitter() remoteRpc._dnode.getCall(0).args[0](client, connection) expect(remoteRpc._connections[d._id]).to.equal(client) connection.emit('error') expect(remoteRpc._connections[d._id]).to.not.exist expect(d.destroy.called).to.be.true expect(stream.destroy.called).to.be.true done() }) }) it('should remove a client connection when the stream errors', function (done) { var stream = new EventEmitter() stream.pipe = sinon.stub() stream.pipe.returnsArg(0) var d = { pipe: sinon.stub(), on: sinon.stub(), destroy: sinon.stub() } d.pipe.returnsArg(0) remoteRpc._dnode.returns(d) remoteRpc._pem.createCertificate.callsArgWith(1, undefined, { serviceKey: 'key', certificate: 'certificate' }) var server = { listen: sinon.stub(), on: sinon.stub() } server.listen.withArgs( remoteRpc._config.remote.port, remoteRpc._config.remote.host, sinon.match.func ).callsArgWith(2, undefined) remoteRpc._tls.createServer.callsArgWith(1, stream).returns(server) remoteRpc._dnode.returns(d) remoteRpc.afterPropertiesSet(function (error) { expect(error).to.not.exist var client = {} var connection = new EventEmitter() connection.id = 'foo' connection.stream = new EventEmitter() remoteRpc._dnode.getCall(0).args[0](client, connection) expect(remoteRpc._connections[d._id]).to.equal(client) stream.emit('error') expect(remoteRpc._connections[d._id]).to.not.exist expect(d.destroy.called).to.be.true done() }) }) it('should remove a client connection when dnode errors', function (done) { var stream = { pipe: sinon.stub().returnsArg(0), on: sinon.stub(), destroy: sinon.stub() } var d = new EventEmitter() d.pipe = sinon.stub() d.pipe.returnsArg(0) d.pipe.returnsArg(0) remoteRpc._dnode.returns(d) remoteRpc._pem.createCertificate.callsArgWith(1, undefined, { serviceKey: 'key', certificate: 'certificate' }) var server = { listen: sinon.stub(), on: sinon.stub() } server.listen.withArgs( remoteRpc._config.remote.port, remoteRpc._config.remote.host, sinon.match.func ).callsArgWith(2, undefined) remoteRpc._tls.createServer.callsArgWith(1, stream).returns(server) remoteRpc._dnode.returns(d) remoteRpc.afterPropertiesSet(function (error) { expect(error).to.not.exist var client = {} var connection = new EventEmitter() connection.id = 'foo' connection.stream = new EventEmitter() remoteRpc._dnode.getCall(0).args[0](client, connection) expect(remoteRpc._connections[d._id]).to.equal(client) d.emit('error') expect(remoteRpc._connections[d._id]).to.not.exist expect(stream.destroy.called).to.be.true done() }) }) it('should return server details to the client', function (done) { remoteRpc._pem.createCertificate.callsArgWith(1, undefined, { serviceKey: 'key', certificate: 'certificate' }) var server = { listen: sinon.stub(), on: sinon.stub() } server.listen.withArgs( remoteRpc._config.remote.port, remoteRpc._config.remote.host, sinon.match.func ).callsArgWith(2, undefined) var stream = new EventEmitter() stream.pipe = sinon.stub() stream.pipe.returnsArg(0) remoteRpc._tls.createServer.callsArgWith(1, stream).returns(server) var d = { pipe: sinon.stub(), on: sinon.stub() } d.pipe.returnsArg(0) remoteRpc._dnode.returns(d) remoteRpc.afterPropertiesSet(function (error) { expect(error).to.not.exist var client = {} var connection = new EventEmitter() connection.id = 'foo' connection.stream = new EventEmitter() var hostname = 'hostname' var type = 'type' var platform = 'plaform' var arch = 'arch' var release = 'release' remoteRpc._package.version = '5.0' remoteRpc._os.hostname.returns(hostname) remoteRpc._os.type.returns(type) remoteRpc._os.platform.returns(platform) remoteRpc._os.arch.returns(arch) remoteRpc._os.release.returns(release) remoteRpc._child_process.exec.withArgs('uname -a').callsArgWithAsync(1, undefined, 'Darwin Alexs-MBP.home 14.1.0 Darwin Kernel Version 14.1.0: Mon Dec 22 23:10:38 PST 2014; root:xnu-2782.10.72~2/RELEASE_X86_64 x86_64') var dnode = {} remoteRpc._checkSignature = function (callback) { callback.apply(null, Array.prototype.slice.call(arguments, 1)) } remoteRpc._dnode.getCall(0).args[0].call(dnode, client, connection) dnode.getDetails({}, function (error, details) { expect(error).to.not.exist expect(details.hostname).to.equal(hostname) expect(details.type).to.equal(type) expect(details.platform).to.equal(platform) expect(details.arch).to.equal(arch) expect(details.release).to.equal(release) expect(details.guvnor).to.equal(remoteRpc._package.version) expect(details.os).to.equal('darwin') done() }) }) }) it('should check an incoming message signature', function (done) { var principal = 'principal' var date = 'date' var hash = 'hash' var nonce = 'nonce' var user = { secret: 'shh', name: principal } var signature = { principal: principal, date: date, hash: hash, nonce: nonce } var userDetails = {} remoteRpc._userDetailsStore.findOrCreate.withArgs('name', user.name, [user.name]).callsArgWithAsync(3, undefined, userDetails) remoteRpc._remoteUserService.findUser.withArgs(principal, sinon.match.func).callsArgWithAsync(1, undefined, user) remoteRpc._crypto.verify.withArgs(signature, user.secret).returns(true) remoteRpc._checkSignature(function (details, one, two, three) { expect(details).to.equal(userDetails) expect(one).to.equal('one') expect(two).to.equal('two') expect(three).to.equal('three') done() }, signature, 'one', 'two', 'three') }) it('should reject message if signature is invalid', function (done) { var principal = 'principal' var date = 'date' var hash = 'hash' var nonce = 'nonce' var signature = { principal: principal, date: date, hash: hash } remoteRpc._checkSignature(function (one, two, three) { }, signature, 'one', 'two', 'three', function (error) { expect(error.code).to.equal('INVALIDSIGNATURE') done() }) }) it('should reject message if user is unknown', function (done) { var principal = 'principal' var date = 'date' var hash = 'hash' var nonce = 'nonce' var signature = { principal: principal, date: date, hash: hash, nonce: nonce } remoteRpc._remoteUserService.findUser.withArgs(principal, sinon.match.func).callsArg(1) remoteRpc._checkSignature(function (one, two, three) { }, signature, 'one', 'two', 'three', function (error) { expect(error.code).to.equal('INVALIDSIGNATURE') done() }) }) it('should reject message if there is an error looking up the user', function (done) { var principal = 'principal' var date = 'date' var hash = 'hash' var nonce = 'nonce' var signature = { principal: principal, date: date, hash: hash, nonce: nonce } remoteRpc._remoteUserService.findUser.withArgs(principal, sinon.match.func).callsArgWith(1, new Error('urk!')) remoteRpc._checkSignature(function (one, two, three) { }, signature, 'one', 'two', 'three', function (error) { expect(error.code).to.equal('INVALIDSIGNATURE') done() }) }) it('should reject message if verification fails', function (done) { var principal = 'principal' var date = 'date' var hash = 'hash' var nonce = 'nonce' var user = { secret: 'shh' } var signature = { principal: principal, date: date, hash: hash, nonce: nonce } var userDetails = {} remoteRpc._userDetailsStore.findOrCreate.withArgs('name', user.name, [user.name]).callsArgWithAsync(3, undefined, userDetails) remoteRpc._remoteUserService.findUser.withArgs(principal, sinon.match.func).callsArgWith(1, undefined, user) remoteRpc._crypto.verify.withArgs(signature, user.secret).returns(false) remoteRpc._checkSignature(function (details, one, two, three) { }, signature, 'one', 'two', 'three', function (error) { expect(error.code).to.equal('INVALIDSIGNATURE') done() }) }) it('should connect to a process and invoke a method', function (done) { var id = 'id' var user = {} var processInfo = {} var childProcess = new EventEmitter() childProcess.kill = sinon.stub() childProcess.stdout = new EventEmitter() childProcess.stdout.end = sinon.stub() childProcess.stdout.pipe = sinon.stub().returnsArg(0) childProcess.stdin = new EventEmitter() var userDetails = {} var dnode = new EventEmitter() dnode.pipe = sinon.stub().returnsArg(0) remoteRpc._dnode.returns(dnode) remoteRpc._guvnor.findProcessInfoById.withArgs(userDetails, id, sinon.match.func).callsArgWith(2, undefined, processInfo) remoteRpc._child_process.fork.returns(childProcess) remoteRpc.connectToProcess(userDetails, id, function (error, remote) { expect(error).to.not.exist // should have exposed methods expect(remote.restart).to.be.a('function') expect(remote.disconnect).to.be.a('function') // invoke exposed method remote.disconnect(function () { expect(childProcess.kill.calledOnce).to.be.true done() }) }, user) // tell the parent we are ready childProcess.emit('message', { type: 'remote:ready' }) // make like dnode has connected to the tunnel dnode.emit('remote', { restart: sinon.stub() }) }) it('should handle an internal error connecting to a process', function (done) { var id = 'id' var message = 'message' var code = 'code' var stack = 'stack' var user = {} var processInfo = {} var childProcess = new EventEmitter() childProcess.kill = sinon.stub() childProcess.stdout = new EventEmitter() childProcess.stdout.end = sinon.stub() childProcess.stdout.pipe = sinon.stub().returnsArg(0) childProcess.stdin = new EventEmitter() var userDetails = {} remoteRpc._guvnor.findProcessInfoById.withArgs(userDetails, id, sinon.match.func).callsArgWith(2, undefined, processInfo) remoteRpc._child_process.fork.returns(childProcess) remoteRpc.connectToProcess(userDetails, id, function (error) { expect(error).to.be.ok expect(error.message).to.equal(message) expect(error.stack).to.equal(stack) expect(error.code).to.equal(code) done() }, user) // simulate error childProcess.emit('message', { type: 'remote:error', args: [{ message: message, code: code, stack: stack }] }) }) it('should connect to a process and handle uncaught error in process', function (done) { var id = 'id' var user = {} var processInfo = {} var childProcess = new EventEmitter() childProcess.kill = sinon.stub() childProcess.stdout = new EventEmitter() childProcess.stdout.end = sinon.stub() childProcess.stdout.pipe = sinon.stub().returnsArg(0) childProcess.stdin = new EventEmitter() var userDetails = {} var dnode = new EventEmitter() dnode.pipe = sinon.stub().returnsArg(0) remoteRpc._dnode.returns(dnode) remoteRpc._guvnor.findProcessInfoById.withArgs(userDetails, id, sinon.match.func).callsArgWith(2, undefined, processInfo) remoteRpc._child_process.fork.returns(childProcess) remoteRpc.connectToProcess(userDetails, id, function (error) { expect(error.message).to.contain('oops') done() }, user) // simulate the child process emitting an error childProcess.emit('error', new Error('oops')) }) it('should connect to a process and handle non-zero exit code', function (done) { var id = 'id' var user = {} var processInfo = {} var childProcess = new EventEmitter() childProcess.kill = sinon.stub() childProcess.stdout = new EventEmitter() childProcess.stdout.end = sinon.stub() childProcess.stdout.pipe = sinon.stub().returnsArg(0) childProcess.stdin = new EventEmitter() var userDetails = {} var dnode = new EventEmitter() dnode.pipe = sinon.stub().returnsArg(0) remoteRpc._dnode.returns(dnode) remoteRpc._guvnor.findProcessInfoById.withArgs(userDetails, id, sinon.match.func).callsArgWith(2, undefined, processInfo) remoteRpc._child_process.fork.returns(childProcess) remoteRpc.connectToProcess(userDetails, id, function (error) { expect(error.message).to.contain('with code') done() }, user) // simulate the child process exiting badly childProcess.emit('exit', 1) }) it('should start mdns advert', function () { var advert = new EventEmitter() advert.start = sinon.stub() var value = true remoteRpc._config.remote.advertise = true remoteRpc._mdns.createAdvertisement.withArgs(value, remoteRpc.port).returns(advert) remoteRpc._mdns.tcp.withArgs('guvnor-rpc').returns(value) remoteRpc._startMdnsAdvertisment() expect(remoteRpc._mdns.createAdvertisement.called).to.be.true expect(advert.start.called).to.be.true }) it('should survive mdns advert failure', function () { var value = true remoteRpc._config.remote.advertise = true remoteRpc._mdns.createAdvertisement.throws(new Error('urk!')) remoteRpc._mdns.tcp.withArgs('guvnor-rpc').returns(value) remoteRpc._startMdnsAdvertisment() }) it('should survive mdns advert emitting error', function () { var advert = new EventEmitter() advert.start = sinon.stub() var value = true remoteRpc._config.remote.advertise = true remoteRpc._mdns.createAdvertisement.withArgs(value, remoteRpc.port).returns(advert) remoteRpc._mdns.tcp.withArgs('guvnor-rpc').returns(value) remoteRpc._startMdnsAdvertisment() advert.emit('error', new Error('Urk!')) }) it('should start a process', function (done) { var user = 'foo' var userDetails = { name: user } var script = 'script' var options = { user: user } var processInfo = 'processInfo' var guvnor = { startProcess: sinon.stub().callsArgWith(3, undefined, processInfo), disconnect: sinon.stub() } remoteRpc._fileSystem.getRunDir.returns('run') remoteRpc._connectToRpc = sinon.stub().callsArgWith(3, undefined, guvnor) remoteRpc._startProcess(userDetails, script, options, function (error, proc) { expect(error).to.not.exist expect(proc).to.equal(processInfo) expect(remoteRpc._connectToRpc.calledWith('run/user.socket')).to.be.true done() }) }) it('should start a process as a different user', function (done) { var user = 'foo' var userDetails = { name: user } var script = 'script' var options = { user: 'bar' } var processInfo = 'processInfo' var guvnor = { startProcess: sinon.stub().callsArgWith(3, undefined, processInfo), disconnect: sinon.stub() } remoteRpc._fileSystem.getRunDir.returns('run') remoteRpc._connectToRpc = sinon.stub().callsArgWith(3, undefined, guvnor) remoteRpc._startProcess(userDetails, script, options, function (error, proc) { expect(error).to.not.exist expect(proc).to.equal(processInfo) expect(remoteRpc._connectToRpc.calledWith('run/admin.socket')).to.be.true done() }) }) })