guvnor
Version:
A node process manager that isn't spanners all the way down
816 lines (655 loc) • 22.8 kB
JavaScript
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()
})
})
})