UNPKG

guvnor

Version:

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

816 lines (655 loc) 22.8 kB
var expect = require('chai').expect, sinon = require('sinon'), inherits = require('util').inherits, Guvnor = require('../../../lib/daemon/Guvnor'), ProcessInfo = require('../../../lib/daemon/domain/ProcessInfo'), EventEmitter = require('events').EventEmitter process.setMaxListeners(0) describe('Guvnor', function () { var guvnor, exit beforeEach(function () { exit = process.exit process.exit = sinon.stub() guvnor = new Guvnor() guvnor._logger = { info: sinon.stub(), warn: sinon.stub(), error: sinon.stub(), debug: sinon.stub() } guvnor._processService = { startProcess: sinon.stub(), listProcesses: sinon.stub(), findByPid: sinon.stub(), findById: sinon.stub(), findByName: sinon.stub(), on: sinon.stub(), killAll: sinon.stub(), removeProcess: sinon.stub() } guvnor._cpuStats = sinon.stub() guvnor._config = { guvnor: {} } guvnor._os = { uptime: sinon.stub(), freemem: sinon.stub(), totalmem: sinon.stub(), cpus: sinon.stub(), hostname: sinon.stub() } guvnor._nodeInspectorWrapper = { stopNodeInspector: sinon.stub() } guvnor._fs = { writeFile: sinon.stub(), readFile: sinon.stub() } guvnor._processInfoStore = { save: sinon.stub(), saveSync: sinon.stub(), all: sinon.stub(), find: sinon.stub() } guvnor._remoteUserService = { findOrCreateUser: sinon.stub(), createUser: sinon.stub(), removeUser: sinon.stub(), listUsers: sinon.stub(), rotateKeys: sinon.stub() } guvnor._processInfoStoreFactory = { create: sinon.stub() } guvnor._pem = { createCertificate: sinon.stub() } guvnor._ini = { parse: sinon.stub(), stringify: sinon.stub() } guvnor._appService = { deploy: sinon.stub(), remove: sinon.stub(), list: sinon.stub(), switchRef: sinon.stub(), listRefs: sinon.stub(), findByName: sinon.stub(), updateRefs: sinon.stub() } guvnor._etc_passwd = { getGroups: sinon.stub() } guvnor._posix = { getgrnam: sinon.stub(), getpwnam: sinon.stub() } }) afterEach(function () { process.exit = exit }) it('should return a list of running processes', function (done) { var processes = [] var numProcesses = Math.floor(Math.random() * 20) // Create new process mocks for (var i = 0; i < numProcesses; i++) { processes.push({ status: 'running', script: 'foo.js', remote: { reportStatus: function (callback) { setTimeout(function () { callback(undefined, { pid: Math.floor(Math.random() * 138) }) }, Math.floor(Math.random() * 4)) } } }) } guvnor._processService.listProcesses.returns(processes) // Method under test guvnor.listProcesses({}, function (error, procs) { expect(error).to.not.exist expect(procs.length).to.equal(processes.length) done() }) }) it("should return a list of running processes, even if a process doesn't reply", function (done) { this.timeout(10000) var processes = [] var numProcesses = Math.floor(Math.random() * 20) // Create new process mocks for (var i = 0; i < numProcesses; i++) { processes.push({ status: 'running', script: 'foo.js', remote: { reportStatus: function (callback) { var error = new Error('Timed out') error.code = 'TIMEOUT' callback(error) } } }) } processes.push({ status: 'paused', script: 'foo.js' }) guvnor._processes = processes guvnor._processService.listProcesses.returns(processes) // Method under test guvnor.listProcesses({}, function (error, procs) { expect(error).to.not.exist expect(procs.length).to.be.equal(processes.length) done() }) }) it('should delegate to process manger to start a process', function () { guvnor.startProcess({}, 'script', 'options', 'callback') expect(guvnor._processService.startProcess.calledWith('script', 'options', 'callback')).to.be.true }) it('should return server status', function (done) { guvnor._config.remote = { inspector: { enabled: false } } guvnor._cpuStats.callsArgWith(0, undefined, [6, 7]) guvnor._os.uptime.returns(5) guvnor._os.freemem.returns(5) guvnor._os.totalmem.returns(5) guvnor._os.cpus.returns([{}, {}]) guvnor._etc_passwd.getGroups.callsArgWith(0, undefined, [{groupname: 'foo'}, {groupname: '_bar'}]) guvnor._posix.getgrnam.withArgs('foo').returns({members: ['baz', '_quux']}) guvnor._posix.getgrnam.withArgs('_bar').returns({members: ['qux']}) guvnor.getServerStatus({}, function (error, status) { expect(error).to.not.exist expect(status.time).to.be.a('number') expect(status.uptime).to.be.a('number') expect(status.freeMemory).to.be.a('number') expect(status.totalMemory).to.be.a('number') expect(status.cpus).to.be.an('array') expect(status.debuggerPort).to.not.exist expect(status.cpus[0].load).to.equal(6) expect(status.cpus[1].load).to.equal(7) done() }) }) it('should return server status with debug port', function (done) { guvnor._config.remote = { inspector: { enabled: true } } guvnor._cpuStats.callsArgWith(0, undefined, []) guvnor._os.uptime.returns(5) guvnor._os.freemem.returns(5) guvnor._os.totalmem.returns(5) guvnor._os.cpus.returns([]) guvnor._nodeInspectorWrapper.debuggerPort = 5 guvnor._etc_passwd.getGroups.callsArgWith(0, undefined, []) guvnor.getServerStatus({}, function (error, status) { expect(error).to.not.exist expect(status.time).to.be.a('number') expect(status.uptime).to.be.a('number') expect(status.freeMemory).to.be.a('number') expect(status.totalMemory).to.be.a('number') expect(status.cpus).to.be.an('array') expect(status.debuggerPort).to.equal(5) done() }) }) it('should find a process by id', function (done) { guvnor._processService.findById.withArgs('foo').returns('bar') guvnor.findProcessInfoById({}, 'foo', function (error, process) { expect(error).to.not.exist expect(process).to.equal('bar') done() }) }) it('should find a process by pid', function (done) { guvnor._processService.findByPid.withArgs('foo').returns('bar') guvnor.findProcessInfoByPid({}, 'foo', function (error, process) { expect(error).to.not.exist expect(process).to.equal('bar') done() }) }) it('should find a process by name', function (done) { guvnor._processService.findByName.withArgs('foo').returns('bar') guvnor.findProcessInfoByName({}, 'foo', function (error, process) { expect(error).to.not.exist expect(process).to.equal('bar') done() }) }) it('should dump processes', function (done) { guvnor._processInfoStore.save.callsArg(0) guvnor.dumpProcesses({}, function (error) { expect(error).to.not.exist expect(guvnor._processInfoStore.save.callCount).to.equal(1) done() }) }) it('should restore processes', function (done) { var store = new EventEmitter() store.all = sinon.stub() store.all.returns([{script: 'foo'}, {script: 'bar'}]) guvnor._processInfoStoreFactory.create.withArgs(['processInfoFactory', 'processes.json'], sinon.match.func).callsArgWith(1, undefined, store) guvnor._processService.startProcess.callsArg(2) guvnor.restoreProcesses({}, function (error, processes) { expect(error).to.not.exist expect(processes.length).to.equal(2) done() }) // triggers invocation of callback passed to restoreProcesses store.emit('loaded') }) it('should return remote host config', function (done) { guvnor._config.guvnor = { user: 'foo' } guvnor._config.remote = { port: 5 } guvnor._os.hostname.returns('bar') guvnor._remoteUserService.findOrCreateUser.withArgs('foo', sinon.match.func).callsArgWith(1, undefined, { secret: 'shhh' }) guvnor.remoteHostConfig({}, function (error, hostname, port, user, secret) { expect(error).to.not.exist expect(hostname).to.equal('bar') expect(port).to.equal(5) expect(user).to.equal('foo') expect(secret).to.equal('shhh') done() }) }) it('should add a remote user', function (done) { guvnor._remoteUserService.createUser.withArgs('foo', sinon.match.func).callsArgWith(1, undefined, 'great success') guvnor.addRemoteUser({}, 'foo', function (error, result) { expect(error).to.not.exist expect(result).to.equal('great success') done() }) }) it('should remove a remote user', function (done) { guvnor._config.guvnor = { user: 'foo' } guvnor._remoteUserService.removeUser.withArgs('notFoo', sinon.match.func).callsArgWith(1, undefined, 'great success') guvnor.removeRemoteUser({}, 'notFoo', function (error, result) { expect(error).to.not.exist expect(result).to.equal('great success') done() }) }) it('should refuse to remove remote daemon user', function (done) { guvnor._config.guvnor = { user: 'foo' } guvnor.removeRemoteUser({}, 'foo', function (error) { expect(error).to.be.ok expect(error.code).to.equal('WILLNOTREMOVEGUVNORUSER') done() }) }) it('should list remote users', function (done) { guvnor._remoteUserService.listUsers.withArgs(sinon.match.func).callsArgWith(0, undefined, 'great success') guvnor.listRemoteUsers({}, function (error, result) { expect(error).to.not.exist expect(result).to.equal('great success') done() }) }) it('should rotate remote keys', function (done) { guvnor._remoteUserService.rotateKeys.withArgs('foo', sinon.match.func).callsArgWith(1, undefined, 'great success') guvnor.rotateRemoteUserKeys({}, 'foo', function (error, result) { expect(error).to.not.exist expect(result).to.equal('great success') done() }) }) it('should delegate to app service for deploying applications', function () { var name = 'name' var url = 'url' var user = 'user' var onOut = 'onOut' var onErr = 'onErr' var callback = 'callback' guvnor.deployApplication({}, name, url, user, onOut, onErr, callback) expect(guvnor._appService.deploy.calledWith(name, url, user, onOut, onErr, callback)).to.be.true }) it('should delegate to app service for removing applications', function () { var name = 'name' var callback = 'callback' guvnor.removeApplication({}, name, callback) expect(guvnor._appService.remove.calledWith(name, callback)).to.be.true }) it('should delegate to app service for listing applications', function () { var callback = 'callback' guvnor.listApplications({}, callback) expect(guvnor._appService.list.calledWith(callback)).to.be.true }) it('should delegate to app service for switching application refs', function () { var name = 'name' var ref = 'ref' var onOut = 'onOut' var onErr = 'onErr' var callback = 'callback' guvnor.switchApplicationRef({}, name, ref, onOut, onErr, callback) expect(guvnor._appService.switchRef.calledWith(name, ref, onOut, onErr)).to.be.true }) it('should delegate to app service for updating application refs', function () { var name = 'name' var ref = 'ref' var onOut = 'onOut' var onErr = 'onErr' var callback = 'callback' guvnor.updateApplicationRefs({}, name, onOut, onErr, callback) expect(guvnor._appService.updateRefs.calledWith(name, onOut, onErr)).to.be.true }) it('should delegate to app service for listing application refs', function () { var name = 'name' var callback = 'callback' guvnor.listApplicationRefs({}, name, callback) expect(guvnor._appService.listRefs.calledWith(name, callback)).to.be.true }) it('should generate ssl keys', function (done) { var config = { guvnor: { confdir: 'conf' } } guvnor._config.guvnor.confdir = 'conf' guvnor._pem.createCertificate.callsArgWith(1, undefined, { serviceKey: 'key', certificate: 'cert' }) guvnor._fs.writeFile.withArgs('conf/rpc.key', 'key').callsArg(3) guvnor._fs.writeFile.withArgs('conf/rpc.cert', 'cert').callsArg(3) guvnor._fs.readFile.withArgs('conf/guvnor', 'utf-8').callsArgWith(2, undefined, 'ini') guvnor._ini.parse.withArgs('ini').returns(config) guvnor._ini.stringify.withArgs(config).returns('ini-updated') guvnor._fs.writeFile.withArgs('conf/guvnor', 'ini-updated').callsArg(3) guvnor.generateRemoteRpcCertificates({}, 10, function (error, path) { expect(error).to.not.exist expect(path).to.equal('conf/guvnor') done() }) }) it('should not write files if generating ssl keys fails', function (done) { guvnor._pem.createCertificate.callsArgWith(1, new Error('urk!')) guvnor.generateRemoteRpcCertificates({}, 10, function (error) { expect(error).to.be.ok expect(guvnor._fs.writeFile.called).to.be.false done() }) }) it('should not read config if writing key/cert files fails', function (done) { guvnor._config.guvnor.confdir = 'conf' guvnor._pem.createCertificate.callsArgWith(1, undefined, { serviceKey: 'key', certificate: 'cert' }) guvnor._fs.writeFile.withArgs('conf/rpc.key', 'key').callsArg(3) guvnor._fs.writeFile.withArgs('conf/rpc.cert', 'cert').callsArgWith(3, new Error('urk!')) guvnor.generateRemoteRpcCertificates({}, 10, function (error) { expect(error).to.be.ok expect(guvnor._fs.readFile.called).to.be.false done() }) }) it('should not write config if reading config fails', function (done) { guvnor._config.guvnor.confdir = 'conf' guvnor._pem.createCertificate.callsArgWith(1, undefined, { serviceKey: 'key', certificate: 'cert' }) guvnor._fs.writeFile.withArgs('conf/rpc.key', 'key').callsArg(3) guvnor._fs.writeFile.withArgs('conf/rpc.cert', 'cert').callsArg(3) guvnor._fs.readFile.withArgs('conf/guvnor', 'utf-8').callsArgWith(2, new Error('urk!'), 'ini') guvnor.generateRemoteRpcCertificates({}, 10, function (error) { expect(error).to.be.ok expect(guvnor._fs.writeFile.calledWith('conf/guvnor')).to.be.false done() }) }) it('should kill the daemon', function () { guvnor._config.guvnor.autoresume = false guvnor._kill = sinon.stub().callsArg(0) var callback = sinon.stub() guvnor.kill({}, callback) expect(callback.called).to.be.true expect(process.exit.called).to.be.true }) it('should delegate to process service for removing processes', function (done) { var id = 'foo' guvnor._processService.removeProcess.callsArg(1) guvnor.removeProcess({}, id, function () { expect(guvnor._processService.removeProcess.calledWith(id)).to.be.true done() }) }) it('should override script, app and name with app details', function (done) { var script = 'foo' var appInfo = { path: 'bar', id: 'baz', name: 'qux' } guvnor._appService.findByName.withArgs(script).returns(appInfo) guvnor._processService.startProcess.callsArg(2) guvnor.startProcess({}, script, {}, function () { expect(guvnor._processService.startProcess.getCall(0).args[1].script).to.equal(appInfo.path) expect(guvnor._processService.startProcess.getCall(0).args[1].app).to.equal(appInfo.id) expect(guvnor._processService.startProcess.getCall(0).args[1].name).to.equal(appInfo.name) done() }) }) it('should start process as different user', function (done) { var script = 'foo' var options = 'opts' guvnor._processService.startProcess.callsArg(2) guvnor.startProcessAsUser({}, script, options, function () { expect(guvnor._processService.startProcess.getCall(0).args[0]).to.equal(script) expect(guvnor._processService.startProcess.getCall(0).args[1]).to.equal(options) done() }) }) it('should autoresume processes', function (done) { var processes = ['foo'] guvnor._config.guvnor.autoresume = true guvnor._processInfoStore.all.returns(processes) guvnor._processService.startProcess.callsArg(1) guvnor.afterPropertiesSet(function () { expect(guvnor._processService.startProcess.getCall(0).args[0]).to.equal(processes[0]) done() }) }) it('should survive autoresumed processes failing to resume', function (done) { var processes = ['foo', 'bar'] guvnor._config.guvnor.autoresume = true guvnor._processInfoStore.all.returns(processes) guvnor._processService.startProcess.withArgs('foo').callsArgWith(1, new Error('urk')) guvnor._processService.startProcess.withArgs('bar').callsArg(1) guvnor.afterPropertiesSet(function () { expect(guvnor._processService.startProcess.getCall(0).args[0]).to.equal(processes[0]) expect(guvnor._processService.startProcess.getCall(1).args[0]).to.equal(processes[1]) done() }) }) it('should not autoresume processes', function (done) { var processes = ['foo'] guvnor._config.guvnor.autoresume = false guvnor._processInfoStore.all.returns(processes) guvnor._processService.startProcess.callsArg(1) guvnor.afterPropertiesSet(function () { expect(guvnor._processService.startProcess.called).to.be.false done() }) }) it('should list users', function (done) { var groups = [{ groupname: 'foo' }, { groupname: '_bar' }, { groupname: 'baz' }] var qux = { name: 'qux', uid: 'qux-uid', gid: 'qux-gid' } var garply = { name: 'garply', uid: 'garply-uid', gid: 'garply-gid' } var group = { name: 'group' } guvnor._posix.getgrnam.withArgs(qux.gid).returns(group) guvnor._posix.getgrnam.withArgs(garply.gid).returns(group) guvnor._etc_passwd.getGroups.callsArgWith(0, undefined, groups) guvnor._posix.getgrnam.withArgs('foo').returns({ members: ['qux', '_quux'] }) guvnor._posix.getgrnam.withArgs('baz').returns({ members: ['qux', 'garply'] }) guvnor._posix.getpwnam.withArgs('qux').returns(qux) guvnor._posix.getpwnam.withArgs('garply').returns(garply) guvnor.listUsers(null, function (error, users) { expect(error).to.not.exist expect(users.length).to.equal(2) expect(users[0].name).to.equal(qux.name) expect(users[0].group).to.equal(group.name) expect(users[0].groups).to.deep.equal(['foo', 'baz', 'group']) expect(users[1].name).to.equal(garply.name) expect(users[1].group).to.equal(group.name) expect(users[1].groups).to.deep.equal(['baz', 'group']) expect(users[0].groups).to.contain(users[0].group) done() }) }) it('should stop a process', function (done) { var user = { name: 'user' } var processInfo = { id: 'id', status: 'starting', user: user.name, process: { emit: sinon.stub(), kill: sinon.stub() } } guvnor._processService.findById.withArgs(processInfo.id).returns(processInfo) guvnor.stopProcess(user, processInfo.id, function (error) { expect(error).to.not.exist expect(processInfo.process.emit.calledWith('process:stopping')).to.be.true expect(processInfo.process.kill.called).to.be.true done() }) }) it('should not stop a running process', function (done) { var user = { name: 'user' } var processInfo = { id: 'id', status: 'running', user: user.name, process: { emit: sinon.stub(), kill: sinon.stub() } } guvnor._processService.findById.withArgs(processInfo.id).returns(processInfo) guvnor.stopProcess(user, processInfo.id, function (error) { expect(error.message).to.contain('running') expect(processInfo.process.kill.called).to.be.false done() }) }) it('should not stop a non-existant process', function (done) { var user = { name: 'user' } guvnor._processService.findById.returns(null) guvnor.stopProcess(user, 'foo', function (error) { expect(error.message).to.contain('No process found') done() }) }) it('should not stop a process that belongs to a different user', function (done) { var user = { name: 'user', group: 'group', groups: ['groups'] } var processInfo = { id: 'id', status: 'starting', user: 'foo', group: 'bar', process: { emit: sinon.stub(), kill: sinon.stub() } } guvnor._processService.findById.withArgs(processInfo.id).returns(processInfo) guvnor.stopProcess(user, processInfo.id, function (error) { expect(error.code).to.equal('EPERM') expect(processInfo.process.kill.called).to.be.false done() }) }) it('should stop a process when called by guvnor user', function (done) { guvnor._config.guvnor.user = 'user' var user = { name: guvnor._config.guvnor.user } var processInfo = { id: 'id', status: 'starting', user: 'other', process: { emit: sinon.stub(), kill: sinon.stub() } } guvnor._processService.findById.withArgs(processInfo.id).returns(processInfo) guvnor.stopProcess(user, processInfo.id, function (error) { expect(error).to.not.exist expect(processInfo.process.emit.calledWith('process:stopping')).to.be.true expect(processInfo.process.kill.called).to.be.true done() }) }) it('should not stop an uninitialised process', function (done) { guvnor._config.guvnor.user = 'user' var user = { name: guvnor._config.guvnor.user } var processInfo = { id: 'id', status: 'starting', user: 'other', process: { emit: sinon.stub() } } guvnor._processService.findById.withArgs(processInfo.id).returns(processInfo) guvnor.stopProcess(user, processInfo.id, function (error) { expect(error.message).to.contain('Could not kill') expect(processInfo.process.emit.calledWith('process:stopping')).to.be.false done() }) }) })