navy
Version:
Quick and powerful development environments using Docker and Docker Compose
430 lines (428 loc) • 16.4 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _chai = require("chai");
var _sinon = _interopRequireDefault(require("sinon"));
var _jsYaml = _interopRequireDefault(require("js-yaml"));
var _dockerClient = _interopRequireDefault(require("../../../util/docker-client"));
var _fs = _interopRequireDefault(require("../../../util/fs"));
var execAsyncModule = _interopRequireWildcard(require("../../../util/exec-async"));
var driverLogging = _interopRequireWildcard(require("../../../driver-logging"));
var _service = require("../../../service");
var _index = _interopRequireDefault(require("../index"));
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
/* eslint-env mocha */
function makeNavy({
normalisedName = 'env',
configProvider = null
} = {}) {
return {
name: normalisedName,
normalisedName,
getConfigProvider: _sinon.default.stub().resolves(configProvider)
};
}
describe('docker-compose driver', function () {
let sandbox;
let originalHome;
let execStub;
beforeEach(function () {
sandbox = _sinon.default.createSandbox();
originalHome = process.env.HOME;
process.env.HOME = '/home/test';
execStub = sandbox.stub(execAsyncModule, 'execAsync').resolves('');
sandbox.stub(driverLogging, 'log');
sandbox.stub(_fs.default, 'statAsync').resolves({});
});
afterEach(function () {
sandbox.restore();
if (originalHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = originalHome;
}
});
describe('launch', function () {
it('should run "docker compose up -d" with the given services and no extra args by default', async function () {
const driver = (0, _index.default)(makeNavy());
await driver.launch(['api']);
const [, args] = execStub.firstCall.args;
(0, _chai.expect)(args).to.include('up');
(0, _chai.expect)(args).to.include('-d');
(0, _chai.expect)(args).to.include('api');
});
it('should map opts.noDeps to --no-deps and opts.forceRecreate to --force-recreate', async function () {
const driver = (0, _index.default)(makeNavy());
await driver.launch(['api'], {
noDeps: true,
forceRecreate: true
});
const [, args] = execStub.firstCall.args;
(0, _chai.expect)(args).to.include('--no-deps');
(0, _chai.expect)(args).to.include('--force-recreate');
});
it('should ignore unknown opts keys', async function () {
const driver = (0, _index.default)(makeNavy());
await driver.launch(['api'], {
unknownOption: true
});
const [, args] = execStub.firstCall.args;
(0, _chai.expect)(args.includes('unknownOption')).to.equal(false);
});
it('should default services to an empty array if not given', async function () {
const driver = (0, _index.default)(makeNavy());
await driver.launch(undefined, {});
const [, args] = execStub.firstCall.args;
(0, _chai.expect)(args.slice(-2)).to.eql(['up', '-d']);
});
it('should default opts to an empty object', async function () {
const driver = (0, _index.default)(makeNavy());
await driver.launch(['api']);
(0, _chai.expect)(execStub.calledOnce).to.equal(true);
});
it('should still call exec when opts is null (treated falsy by checks)', async function () {
const driver = (0, _index.default)(makeNavy());
await driver.launch(['api'], null);
(0, _chai.expect)(execStub.calledOnce).to.equal(true);
});
});
describe('destroy', function () {
it('should run kill followed by down -v', async function () {
const driver = (0, _index.default)(makeNavy());
await driver.destroy();
(0, _chai.expect)(execStub.callCount).to.equal(2);
(0, _chai.expect)(execStub.firstCall.args[1]).to.include('kill');
(0, _chai.expect)(execStub.secondCall.args[1]).to.include('down');
(0, _chai.expect)(execStub.secondCall.args[1]).to.include('-v');
});
});
describe('start/stop/restart/kill', function () {
[{
method: 'start',
cmd: 'start'
}, {
method: 'stop',
cmd: 'stop'
}, {
method: 'restart',
cmd: 'restart'
}, {
method: 'kill',
cmd: 'kill'
}].forEach(({
method,
cmd
}) => {
it(`should call docker compose ${cmd} with the given services`, async function () {
const driver = (0, _index.default)(makeNavy());
await driver[method](['api']);
const [, args] = execStub.firstCall.args;
(0, _chai.expect)(args).to.include(cmd);
(0, _chai.expect)(args).to.include('api');
});
});
});
describe('rm', function () {
it('should call docker compose rm -f -v with services', async function () {
const driver = (0, _index.default)(makeNavy());
await driver.rm(['api']);
const [, args] = execStub.firstCall.args;
(0, _chai.expect)(args).to.include('rm');
(0, _chai.expect)(args).to.include('-f');
(0, _chai.expect)(args).to.include('-v');
(0, _chai.expect)(args).to.include('api');
});
it('should default services to [] when not provided', async function () {
const driver = (0, _index.default)(makeNavy());
await driver.rm();
const [, args] = execStub.firstCall.args;
(0, _chai.expect)(args).to.include('rm');
(0, _chai.expect)(args).to.include('-f');
(0, _chai.expect)(args).to.include('-v');
});
});
describe('update', function () {
it('should pull all services then relaunch only the running ones', async function () {
const navy = makeNavy();
const driver = (0, _index.default)(navy);
sandbox.stub(_dockerClient.default, 'listContainers').resolves([{
Labels: {
'com.docker.compose.service': 'api'
}
}, {
Labels: {
'com.docker.compose.service': 'redis'
}
}]);
await driver.update(['api', 'web', 'redis']);
const pullCall = execStub.getCalls().find(c => c.args[1].includes('pull'));
const relaunchCall = execStub.getCalls().find(c => c.args[1].includes('up') && c.args[1].includes('--no-deps'));
(0, _chai.expect)(pullCall.args[1]).to.include('api');
(0, _chai.expect)(pullCall.args[1]).to.include('web');
(0, _chai.expect)(pullCall.args[1]).to.include('redis');
(0, _chai.expect)(relaunchCall.args[1]).to.include('api');
(0, _chai.expect)(relaunchCall.args[1]).to.include('redis');
(0, _chai.expect)(relaunchCall.args[1].includes('web')).to.equal(false);
});
it('should not call up when no services need relaunching', async function () {
const driver = (0, _index.default)(makeNavy());
sandbox.stub(_dockerClient.default, 'listContainers').resolves([]);
await driver.update(['api']);
const upCall = execStub.getCalls().find(c => c.args[1].includes('--no-deps'));
(0, _chai.expect)(upCall).to.equal(undefined);
});
it('should default services to an empty array when not provided', async function () {
const driver = (0, _index.default)(makeNavy());
sandbox.stub(_dockerClient.default, 'listContainers').resolves([]);
await driver.update();
(0, _chai.expect)(execStub.firstCall.args[1]).to.include('pull');
});
});
describe('spawnLogStream', function () {
it('should call docker compose logs -f --tail=250 services with pipeLog', async function () {
const driver = (0, _index.default)(makeNavy());
await driver.spawnLogStream(['api']);
const [, args,, opts] = execStub.firstCall.args;
(0, _chai.expect)(args).to.include('logs');
(0, _chai.expect)(args).to.include('-f');
(0, _chai.expect)(args).to.include('--tail=250');
(0, _chai.expect)(args).to.include('api');
(0, _chai.expect)(opts.maxBuffer).to.equal(Infinity);
});
it('should default services to [] when not provided', async function () {
const driver = (0, _index.default)(makeNavy());
await driver.spawnLogStream();
const [, args] = execStub.firstCall.args;
(0, _chai.expect)(args).to.include('logs');
});
});
describe('ps', function () {
let listContainersStub;
let getContainerStub;
beforeEach(function () {
listContainersStub = sandbox.stub(_dockerClient.default, 'listContainers');
getContainerStub = sandbox.stub(_dockerClient.default, 'getContainer');
});
it('should map docker inspect output to a ServiceList of running services', async function () {
const driver = (0, _index.default)(makeNavy({
normalisedName: 'env'
}));
listContainersStub.resolves([{
Id: 'c1'
}]);
getContainerStub.returns({
inspect: async () => ({
Id: 'c1-full',
Config: {
Image: 'api:1',
Labels: {
'com.docker.compose.service': 'api'
}
},
State: {
Running: true
}
})
});
const services = await driver.ps();
(0, _chai.expect)(services).to.have.lengthOf(1);
(0, _chai.expect)(services[0]).to.include({
id: 'c1-full',
name: 'api',
image: 'api:1',
status: _service.Status.RUNNING
});
(0, _chai.expect)(services[0].raw).to.have.property('Id', 'c1-full');
});
it('should mark non-running services as EXITED', async function () {
const driver = (0, _index.default)(makeNavy());
listContainersStub.resolves([{
Id: 'c1'
}]);
getContainerStub.returns({
inspect: async () => ({
Id: 'c1',
Config: {
Image: 'a',
Labels: {
'com.docker.compose.service': 'a'
}
},
State: {
Running: false
}
})
});
const services = await driver.ps();
(0, _chai.expect)(services[0].status).to.equal(_service.Status.EXITED);
});
it('should add a service-specific label filter when service is provided', async function () {
const driver = (0, _index.default)(makeNavy({
normalisedName: 'env'
}));
listContainersStub.resolves([]);
await driver.ps('api');
const filter = listContainersStub.firstCall.args[0];
(0, _chai.expect)(filter.filters.label).to.include('com.docker.compose.project=env');
(0, _chai.expect)(filter.filters.label).to.include('com.docker.compose.service=api');
});
});
describe('port', function () {
it('should return the host port from container.NetworkSettings.Ports[<priv>/tcp]', async function () {
const driver = (0, _index.default)(makeNavy());
sandbox.stub(_dockerClient.default, 'listContainers').resolves([{
Id: 'c1'
}]);
sandbox.stub(_dockerClient.default, 'getContainer').returns({
inspect: async () => ({
Id: 'c1',
Config: {
Image: 'a',
Labels: {
'com.docker.compose.service': 'api'
}
},
State: {
Running: true
},
NetworkSettings: {
Ports: {
'80/tcp': [{
HostPort: '8080'
}]
}
}
})
});
(0, _chai.expect)(await driver.port('api', 80)).to.equal(8080);
});
it('should return null when no container is found for the service', async function () {
const driver = (0, _index.default)(makeNavy());
sandbox.stub(_dockerClient.default, 'listContainers').resolves([]);
(0, _chai.expect)(await driver.port('api', 80)).to.equal(null);
});
it('should return null when port mapping is missing or empty', async function () {
const driver = (0, _index.default)(makeNavy());
sandbox.stub(_dockerClient.default, 'listContainers').resolves([{
Id: 'c1'
}]);
sandbox.stub(_dockerClient.default, 'getContainer').returns({
inspect: async () => ({
Id: 'c1',
Config: {
Image: 'a',
Labels: {
'com.docker.compose.service': 'api'
}
},
State: {
Running: true
},
NetworkSettings: {
Ports: {}
}
})
});
(0, _chai.expect)(await driver.port('api', 80)).to.equal(null);
});
it('should default the index argument to 1 when not provided', async function () {
const driver = (0, _index.default)(makeNavy());
sandbox.stub(_dockerClient.default, 'listContainers').resolves([{
Id: 'c1'
}]);
sandbox.stub(_dockerClient.default, 'getContainer').returns({
inspect: async () => ({
Id: 'c1',
Config: {
Image: 'a',
Labels: {
'com.docker.compose.service': 'api'
}
},
State: {
Running: true
},
NetworkSettings: {
Ports: {
'80/tcp': [{
HostPort: '8080'
}]
}
}
})
});
(0, _chai.expect)(await driver.port('api', 80)).to.equal(8080);
});
});
describe('writeConfig', function () {
it('should write the YAML serialised config to the compiled compose path', async function () {
const driver = (0, _index.default)(makeNavy({
normalisedName: 'env'
}));
const writeStub = sandbox.stub(_fs.default, 'writeFileAsync').resolves();
const config = {
version: '3',
services: {
api: {
image: 'api'
}
}
};
await driver.writeConfig(config);
(0, _chai.expect)(writeStub.calledOnce).to.equal(true);
(0, _chai.expect)(writeStub.firstCall.args[0]).to.contain('docker-compose.tmp.yml');
const written = _jsYaml.default.load(writeStub.firstCall.args[1]);
(0, _chai.expect)(written).to.eql(config);
});
});
describe('getConfig', function () {
it('should run docker compose config and parse the YAML output', async function () {
const driver = (0, _index.default)(makeNavy({
configProvider: {
getNavyPath: async () => '/orig'
}
}));
execStub.resolves('version: "3"\nservices:\n api:\n image: api:1\n');
const config = await driver.getConfig();
(0, _chai.expect)(config).to.eql({
version: '3',
services: {
api: {
image: 'api:1'
}
}
});
});
});
describe('getLaunchedServiceNames', function () {
it('should return the compose service names of all running containers', async function () {
const driver = (0, _index.default)(makeNavy({
normalisedName: 'env'
}));
const listStub = sandbox.stub(_dockerClient.default, 'listContainers').resolves([{
Labels: {
'com.docker.compose.service': 'api'
}
}, {
Labels: {
'com.docker.compose.service': 'web'
}
}]);
const names = await driver.getLaunchedServiceNames();
(0, _chai.expect)(names).to.eql(['api', 'web']);
const filter = listStub.firstCall.args[0];
(0, _chai.expect)(filter.filters.label).to.include('com.docker.compose.project=env');
});
});
describe('getAvailableServiceNames', function () {
it('should return the keys of services from the parsed config', async function () {
const driver = (0, _index.default)(makeNavy({
configProvider: {
getNavyPath: async () => '/orig'
}
}));
execStub.resolves('services:\n api:\n image: a\n web:\n image: w\n');
const names = await driver.getAvailableServiceNames();
(0, _chai.expect)(names).to.eql(['api', 'web']);
});
});
});