navy
Version:
Quick and powerful development environments using Docker and Docker Compose
424 lines (422 loc) • 18.2 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _chai = require("chai");
var _sinon = _interopRequireDefault(require("sinon"));
var _os = _interopRequireDefault(require("os"));
var _path = _interopRequireDefault(require("path"));
var _jsYaml = _interopRequireDefault(require("js-yaml"));
var _fs = _interopRequireDefault(require("fs"));
var _dockerClient = _interopRequireDefault(require("../util/docker-client"));
var httpsModule = _interopRequireWildcard(require("../util/https"));
var execAsyncModule = _interopRequireWildcard(require("../util/exec-async"));
var driverLogging = _interopRequireWildcard(require("../driver-logging"));
var navyModule = _interopRequireWildcard(require("../navy"));
var _httpProxy = require("../http-proxy");
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 */
describe('resolveProxyImage', function () {
afterEach(function () {
delete process.env.NAVY_HTTP_PROXY_IMAGE;
});
it('should return the default image when no env var or navyFile config is provided', function () {
(0, _chai.expect)((0, _httpProxy.resolveProxyImage)()).to.equal('navycloud/navy-proxy');
(0, _chai.expect)((0, _httpProxy.resolveProxyImage)(null)).to.equal('navycloud/navy-proxy');
(0, _chai.expect)((0, _httpProxy.resolveProxyImage)(undefined)).to.equal('navycloud/navy-proxy');
});
it('should return the default image when navyFile has no httpProxyImage', function () {
(0, _chai.expect)((0, _httpProxy.resolveProxyImage)({})).to.equal('navycloud/navy-proxy');
(0, _chai.expect)((0, _httpProxy.resolveProxyImage)({
plugins: []
})).to.equal('navycloud/navy-proxy');
});
it('should use httpProxyImage from navyFile when set', function () {
const navyFile = {
httpProxyImage: 'myregistry/custom-proxy:latest'
};
(0, _chai.expect)((0, _httpProxy.resolveProxyImage)(navyFile)).to.equal('myregistry/custom-proxy:latest');
});
it('should use NAVY_HTTP_PROXY_IMAGE env var when set', function () {
process.env.NAVY_HTTP_PROXY_IMAGE = 'envregistry/env-proxy:v2';
(0, _chai.expect)((0, _httpProxy.resolveProxyImage)()).to.equal('envregistry/env-proxy:v2');
});
it('should give NAVY_HTTP_PROXY_IMAGE env var precedence over navyFile httpProxyImage', function () {
process.env.NAVY_HTTP_PROXY_IMAGE = 'envregistry/env-proxy:v2';
const navyFile = {
httpProxyImage: 'myregistry/custom-proxy:latest'
};
(0, _chai.expect)((0, _httpProxy.resolveProxyImage)(navyFile)).to.equal('envregistry/env-proxy:v2');
});
it('should give NAVY_HTTP_PROXY_IMAGE env var precedence over default', function () {
process.env.NAVY_HTTP_PROXY_IMAGE = 'override/image:tag';
(0, _chai.expect)((0, _httpProxy.resolveProxyImage)(null)).to.equal('override/image:tag');
});
it('should fall back to default when httpProxyImage is an empty string', function () {
(0, _chai.expect)((0, _httpProxy.resolveProxyImage)({
httpProxyImage: ''
})).to.equal('navycloud/navy-proxy');
});
it('should fall back to default when NAVY_HTTP_PROXY_IMAGE is an empty string', function () {
process.env.NAVY_HTTP_PROXY_IMAGE = '';
(0, _chai.expect)((0, _httpProxy.resolveProxyImage)()).to.equal('navycloud/navy-proxy');
});
});
describe('resolveProxyEnvFromNavyFile', function () {
it('should return an empty object when no navyFile is provided', function () {
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)()).to.eql({});
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)(null)).to.eql({});
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)(undefined)).to.eql({});
});
it('should return an empty object when navyFile has no httpProxyEnv', function () {
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)({})).to.eql({});
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)({
plugins: []
})).to.eql({});
});
it('should return the httpProxyEnv map from navyFile', function () {
const navyFile = {
httpProxyEnv: {
FOO: 'bar',
BAZ: 'qux'
}
};
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)(navyFile)).to.eql({
FOO: 'bar',
BAZ: 'qux'
});
});
it('should drop entries with empty string, null, or undefined values', function () {
const navyFile = {
httpProxyEnv: {
EMPTY: '',
NULL: null,
UNDEF: undefined,
KEEP: 'value'
}
};
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)(navyFile)).to.eql({
KEEP: 'value'
});
});
it('should coerce numeric values to strings', function () {
const navyFile = {
httpProxyEnv: {
PORT: 8080,
ZERO: 0
}
};
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)(navyFile)).to.eql({
PORT: '8080',
ZERO: '0'
});
});
it('should return an empty object when httpProxyEnv is a string', function () {
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)({
httpProxyEnv: 'FOO=bar'
})).to.eql({});
});
it('should return an empty object when httpProxyEnv is an array', function () {
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)({
httpProxyEnv: ['FOO=bar']
})).to.eql({});
});
});
describe('resolveProxyEnvAllowlist', function () {
const namesSetDuringTest = [];
function setEnv(name, value) {
namesSetDuringTest.push(name);
process.env[name] = value;
}
afterEach(function () {
delete process.env.NAVY_HTTP_PROXY_ENV;
while (namesSetDuringTest.length > 0) {
delete process.env[namesSetDuringTest.pop()];
}
});
it('should return an empty object when NAVY_HTTP_PROXY_ENV is unset', function () {
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({});
});
it('should return an empty object when NAVY_HTTP_PROXY_ENV is empty', function () {
process.env.NAVY_HTTP_PROXY_ENV = '';
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({});
});
it('should forward a single name whose value is present in process.env', function () {
process.env.NAVY_HTTP_PROXY_ENV = 'FORWARD_ME';
setEnv('FORWARD_ME', 'hello');
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({
FORWARD_ME: 'hello'
});
});
it('should trim whitespace from comma-separated names', function () {
process.env.NAVY_HTTP_PROXY_ENV = ' FOO , BAR ';
setEnv('FOO', 'one');
setEnv('BAR', 'two');
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({
FOO: 'one',
BAR: 'two'
});
});
it('should deduplicate repeated names', function () {
process.env.NAVY_HTTP_PROXY_ENV = 'FOO,FOO,BAR,FOO';
setEnv('FOO', 'one');
setEnv('BAR', 'two');
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({
FOO: 'one',
BAR: 'two'
});
});
it('should drop names whose value is empty or unset', function () {
process.env.NAVY_HTTP_PROXY_ENV = 'PRESENT,EMPTY,MISSING';
setEnv('PRESENT', 'value');
setEnv('EMPTY', '');
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({
PRESENT: 'value'
});
});
it('should ignore empty entries produced by trailing or repeated commas', function () {
process.env.NAVY_HTTP_PROXY_ENV = ',FOO,,';
setEnv('FOO', 'value');
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({
FOO: 'value'
});
});
});
describe('resolveProxyEnv', function () {
const namesSetDuringTest = [];
function setEnv(name, value) {
namesSetDuringTest.push(name);
process.env[name] = value;
}
afterEach(function () {
delete process.env.NAVY_HTTP_PROXY_ENV;
while (namesSetDuringTest.length > 0) {
delete process.env[namesSetDuringTest.pop()];
}
});
it('should return null when both sources are empty', function () {
(0, _chai.expect)((0, _httpProxy.resolveProxyEnv)()).to.equal(null);
(0, _chai.expect)((0, _httpProxy.resolveProxyEnv)({})).to.equal(null);
});
it('should return only the navyFile entries when no allowlist is set', function () {
const navyFile = {
httpProxyEnv: {
FOO: 'bar'
}
};
(0, _chai.expect)((0, _httpProxy.resolveProxyEnv)(navyFile)).to.eql({
FOO: 'bar'
});
});
it('should return only the allowlisted entries when navyFile has none', function () {
process.env.NAVY_HTTP_PROXY_ENV = 'FORWARD_ME';
setEnv('FORWARD_ME', 'hello');
(0, _chai.expect)((0, _httpProxy.resolveProxyEnv)()).to.eql({
FORWARD_ME: 'hello'
});
});
it('should merge entries from both sources', function () {
process.env.NAVY_HTTP_PROXY_ENV = 'FROM_ENV';
setEnv('FROM_ENV', 'env-value');
const navyFile = {
httpProxyEnv: {
FROM_FILE: 'file-value'
}
};
(0, _chai.expect)((0, _httpProxy.resolveProxyEnv)(navyFile)).to.eql({
FROM_FILE: 'file-value',
FROM_ENV: 'env-value'
});
});
it('should give the allowlist precedence on key collisions', function () {
process.env.NAVY_HTTP_PROXY_ENV = 'SHARED';
setEnv('SHARED', 'env-value');
const navyFile = {
httpProxyEnv: {
SHARED: 'file-value'
}
};
(0, _chai.expect)((0, _httpProxy.resolveProxyEnv)(navyFile)).to.eql({
SHARED: 'env-value'
});
});
});
describe('resolveDockerSocketPath', function () {
let originalDockerHost;
beforeEach(function () {
originalDockerHost = process.env.DOCKER_HOST;
delete process.env.DOCKER_HOST;
});
afterEach(function () {
if (originalDockerHost === undefined) {
delete process.env.DOCKER_HOST;
} else {
process.env.DOCKER_HOST = originalDockerHost;
}
});
it('should default to /var/run/docker.sock when DOCKER_HOST is not set', function () {
(0, _chai.expect)((0, _httpProxy.resolveDockerSocketPath)()).to.equal('/var/run/docker.sock');
});
it('should default to /var/run/docker.sock when DOCKER_HOST is empty', function () {
process.env.DOCKER_HOST = '';
(0, _chai.expect)((0, _httpProxy.resolveDockerSocketPath)()).to.equal('/var/run/docker.sock');
});
it('should extract the socket path when DOCKER_HOST uses the unix:// scheme', function () {
process.env.DOCKER_HOST = 'unix:///home/runner/setup-docker-action-abc/docker.sock';
(0, _chai.expect)((0, _httpProxy.resolveDockerSocketPath)()).to.equal('/home/runner/setup-docker-action-abc/docker.sock');
});
it('should fall back to default when DOCKER_HOST is unix:// with no path', function () {
process.env.DOCKER_HOST = 'unix://';
(0, _chai.expect)((0, _httpProxy.resolveDockerSocketPath)()).to.equal('/var/run/docker.sock');
});
it('should fall back to default when DOCKER_HOST uses tcp:// scheme', function () {
process.env.DOCKER_HOST = 'tcp://localhost:2375';
(0, _chai.expect)((0, _httpProxy.resolveDockerSocketPath)()).to.equal('/var/run/docker.sock');
});
});
describe('reconfigureHTTPProxy', function () {
let sandbox;
let writeFileSyncStub;
let existsSyncStub;
let listNetworksStub;
let execStub;
let originalDockerHost;
const composeFile = _path.default.join(_os.default.tmpdir(), 'navyinternaldockercompose.yml');
beforeEach(function () {
originalDockerHost = process.env.DOCKER_HOST;
delete process.env.DOCKER_HOST;
sandbox = _sinon.default.createSandbox();
listNetworksStub = sandbox.stub(_dockerClient.default, 'listNetworks');
sandbox.stub(httpsModule, 'getCertsPath').resolves('');
sandbox.stub(driverLogging, 'log');
sandbox.stub(navyModule, 'getLaunchedNavyNames').resolves([]);
execStub = sandbox.stub(execAsyncModule, 'execAsync').resolves('');
writeFileSyncStub = sandbox.stub(_fs.default, 'writeFileSync');
existsSyncStub = sandbox.stub(_fs.default, 'existsSync').returns(false);
});
afterEach(function () {
sandbox.restore();
if (originalDockerHost === undefined) {
delete process.env.DOCKER_HOST;
} else {
process.env.DOCKER_HOST = originalDockerHost;
}
});
it('should default opts.navies via getLaunchedNavyNames when not supplied', async function () {
listNetworksStub.resolves([]);
navyModule.getLaunchedNavyNames.resolves(['navyA']);
await (0, _httpProxy.reconfigureHTTPProxy)();
(0, _chai.expect)(navyModule.getLaunchedNavyNames.calledOnce).to.equal(true);
});
it('should write a docker-compose config containing only matching docker-compose networks', async function () {
listNetworksStub.resolves([{
Name: 'navyA_default'
}, {
Name: 'navyB_default'
}, {
Name: 'host'
}, {
Name: 'navyB_other'
}, {
Name: 'name-without-underscore'
}]);
await (0, _httpProxy.reconfigureHTTPProxy)({
navies: ['navyA']
});
(0, _chai.expect)(writeFileSyncStub.calledOnce).to.equal(true);
(0, _chai.expect)(writeFileSyncStub.firstCall.args[0]).to.equal(composeFile);
const written = _jsYaml.default.load(writeFileSyncStub.firstCall.args[1]);
(0, _chai.expect)(written.networks).to.have.property('navyA_default').that.eql({
external: true
});
(0, _chai.expect)(written.networks).to.not.have.property('navyB_default');
(0, _chai.expect)(written.networks).to.not.have.property('host');
(0, _chai.expect)(written.services['nginx-proxy'].image).to.equal('navycloud/navy-proxy');
(0, _chai.expect)(written.services['nginx-proxy'].ports).to.eql(['80:80']);
(0, _chai.expect)(written.services['nginx-proxy'].volumes[0]).to.equal('/var/run/docker.sock:/tmp/docker.sock:ro');
});
it('should mount certs and expose port 443 when a certs path is available', async function () {
httpsModule.getCertsPath.resolves('/certs');
listNetworksStub.resolves([]);
await (0, _httpProxy.reconfigureHTTPProxy)({
navies: []
});
const written = _jsYaml.default.load(writeFileSyncStub.firstCall.args[1]);
(0, _chai.expect)(written.services['nginx-proxy'].ports).to.eql(['80:80', '443:443']);
(0, _chai.expect)(written.services['nginx-proxy'].volumes).to.include('/certs:/etc/nginx/certs');
(0, _chai.expect)(written.services['nginx-proxy'].volumes).to.include('/certs:/etc/nginx/dhparam');
});
it('should pass the configured proxy image from navyFile through to the compose config', async function () {
listNetworksStub.resolves([]);
await (0, _httpProxy.reconfigureHTTPProxy)({
navies: [],
navyFile: {
httpProxyImage: 'custom/proxy:1'
}
});
const written = _jsYaml.default.load(writeFileSyncStub.firstCall.args[1]);
(0, _chai.expect)(written.services['nginx-proxy'].image).to.equal('custom/proxy:1');
});
it('should run docker compose up after writing the config', async function () {
listNetworksStub.resolves([]);
await (0, _httpProxy.reconfigureHTTPProxy)({
navies: []
});
const upCall = execStub.getCalls().find(c => Array.isArray(c.args[1]) && c.args[1].includes('up'));
// eslint-disable-next-line no-unused-expressions
(0, _chai.expect)(upCall).to.exist;
(0, _chai.expect)(upCall.args[0]).to.equal('docker compose');
(0, _chai.expect)(upCall.args[1]).to.eql(['-f', composeFile, '-p', 'navyinternal', 'up', '-d']);
});
it('should rm the existing nginx-proxy container before reconfiguring when restart=true and the file exists', async function () {
listNetworksStub.resolves([]);
existsSyncStub.withArgs(composeFile).returns(true);
await (0, _httpProxy.reconfigureHTTPProxy)({
navies: [],
restart: true
});
const rmCall = execStub.getCalls().find(c => Array.isArray(c.args[1]) && c.args[1].includes('rm'));
// eslint-disable-next-line no-unused-expressions
(0, _chai.expect)(rmCall).to.exist;
(0, _chai.expect)(rmCall.args[1]).to.eql(['-f', composeFile, '-p', 'navyinternal', 'rm', '-s', '-f', 'nginx-proxy']);
});
it('should skip the rm step when restart=true but no compose file exists yet', async function () {
listNetworksStub.resolves([]);
existsSyncStub.withArgs(composeFile).returns(false);
await (0, _httpProxy.reconfigureHTTPProxy)({
navies: [],
restart: true
});
const rmCall = execStub.getCalls().find(c => Array.isArray(c.args[1]) && c.args[1].includes('rm'));
(0, _chai.expect)(rmCall).to.equal(undefined);
});
it('should omit the environment block from the proxy compose service when no env config is provided', async function () {
listNetworksStub.resolves([]);
await (0, _httpProxy.reconfigureHTTPProxy)({
navies: []
});
const written = _jsYaml.default.load(writeFileSyncStub.firstCall.args[1]);
(0, _chai.expect)(written.services['nginx-proxy']).to.not.have.property('environment');
});
it('should include the merged environment block on the proxy compose service when httpProxyEnv or NAVY_HTTP_PROXY_ENV is set', async function () {
listNetworksStub.resolves([]);
process.env.NAVY_HTTP_PROXY_ENV = 'FROM_ENV';
process.env.FROM_ENV = 'env-value';
try {
await (0, _httpProxy.reconfigureHTTPProxy)({
navies: [],
navyFile: {
httpProxyEnv: {
FROM_FILE: 'file-value'
}
}
});
const written = _jsYaml.default.load(writeFileSyncStub.firstCall.args[1]);
(0, _chai.expect)(written.services['nginx-proxy'].environment).to.eql({
FROM_FILE: 'file-value',
FROM_ENV: 'env-value'
});
} finally {
delete process.env.NAVY_HTTP_PROXY_ENV;
delete process.env.FROM_ENV;
}
});
});