navy 7.0.0-alpha.2 → 7.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/__tests__/http-proxy.js +210 -0
- package/lib/cli/__tests__/program-unknown-options.js +69 -0
- package/lib/cli/__tests__/program.js +115 -0
- package/lib/cli/program.js +5 -0
- package/lib/cli/util/__tests__/merge-action-options.js +24 -0
- package/lib/cli/util/merge-action-options.js +15 -4
- package/lib/http-proxy.js +44 -0
- package/package.json +1 -1
|
@@ -62,6 +62,185 @@ describe('resolveProxyImage', function () {
|
|
|
62
62
|
(0, _chai.expect)((0, _httpProxy.resolveProxyImage)()).to.equal('navycloud/navy-proxy');
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
|
+
describe('resolveProxyEnvFromNavyFile', function () {
|
|
66
|
+
it('should return an empty object when no navyFile is provided', function () {
|
|
67
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)()).to.eql({});
|
|
68
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)(null)).to.eql({});
|
|
69
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)(undefined)).to.eql({});
|
|
70
|
+
});
|
|
71
|
+
it('should return an empty object when navyFile has no httpProxyEnv', function () {
|
|
72
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)({})).to.eql({});
|
|
73
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)({
|
|
74
|
+
plugins: []
|
|
75
|
+
})).to.eql({});
|
|
76
|
+
});
|
|
77
|
+
it('should return the httpProxyEnv map from navyFile', function () {
|
|
78
|
+
const navyFile = {
|
|
79
|
+
httpProxyEnv: {
|
|
80
|
+
FOO: 'bar',
|
|
81
|
+
BAZ: 'qux'
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)(navyFile)).to.eql({
|
|
85
|
+
FOO: 'bar',
|
|
86
|
+
BAZ: 'qux'
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
it('should drop entries with empty string, null, or undefined values', function () {
|
|
90
|
+
const navyFile = {
|
|
91
|
+
httpProxyEnv: {
|
|
92
|
+
EMPTY: '',
|
|
93
|
+
NULL: null,
|
|
94
|
+
UNDEF: undefined,
|
|
95
|
+
KEEP: 'value'
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)(navyFile)).to.eql({
|
|
99
|
+
KEEP: 'value'
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it('should coerce numeric values to strings', function () {
|
|
103
|
+
const navyFile = {
|
|
104
|
+
httpProxyEnv: {
|
|
105
|
+
PORT: 8080,
|
|
106
|
+
ZERO: 0
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)(navyFile)).to.eql({
|
|
110
|
+
PORT: '8080',
|
|
111
|
+
ZERO: '0'
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
it('should return an empty object when httpProxyEnv is a string', function () {
|
|
115
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)({
|
|
116
|
+
httpProxyEnv: 'FOO=bar'
|
|
117
|
+
})).to.eql({});
|
|
118
|
+
});
|
|
119
|
+
it('should return an empty object when httpProxyEnv is an array', function () {
|
|
120
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvFromNavyFile)({
|
|
121
|
+
httpProxyEnv: ['FOO=bar']
|
|
122
|
+
})).to.eql({});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe('resolveProxyEnvAllowlist', function () {
|
|
126
|
+
const namesSetDuringTest = [];
|
|
127
|
+
function setEnv(name, value) {
|
|
128
|
+
namesSetDuringTest.push(name);
|
|
129
|
+
process.env[name] = value;
|
|
130
|
+
}
|
|
131
|
+
afterEach(function () {
|
|
132
|
+
delete process.env.NAVY_HTTP_PROXY_ENV;
|
|
133
|
+
while (namesSetDuringTest.length > 0) {
|
|
134
|
+
delete process.env[namesSetDuringTest.pop()];
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
it('should return an empty object when NAVY_HTTP_PROXY_ENV is unset', function () {
|
|
138
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({});
|
|
139
|
+
});
|
|
140
|
+
it('should return an empty object when NAVY_HTTP_PROXY_ENV is empty', function () {
|
|
141
|
+
process.env.NAVY_HTTP_PROXY_ENV = '';
|
|
142
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({});
|
|
143
|
+
});
|
|
144
|
+
it('should forward a single name whose value is present in process.env', function () {
|
|
145
|
+
process.env.NAVY_HTTP_PROXY_ENV = 'FORWARD_ME';
|
|
146
|
+
setEnv('FORWARD_ME', 'hello');
|
|
147
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({
|
|
148
|
+
FORWARD_ME: 'hello'
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it('should trim whitespace from comma-separated names', function () {
|
|
152
|
+
process.env.NAVY_HTTP_PROXY_ENV = ' FOO , BAR ';
|
|
153
|
+
setEnv('FOO', 'one');
|
|
154
|
+
setEnv('BAR', 'two');
|
|
155
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({
|
|
156
|
+
FOO: 'one',
|
|
157
|
+
BAR: 'two'
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
it('should deduplicate repeated names', function () {
|
|
161
|
+
process.env.NAVY_HTTP_PROXY_ENV = 'FOO,FOO,BAR,FOO';
|
|
162
|
+
setEnv('FOO', 'one');
|
|
163
|
+
setEnv('BAR', 'two');
|
|
164
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({
|
|
165
|
+
FOO: 'one',
|
|
166
|
+
BAR: 'two'
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
it('should drop names whose value is empty or unset', function () {
|
|
170
|
+
process.env.NAVY_HTTP_PROXY_ENV = 'PRESENT,EMPTY,MISSING';
|
|
171
|
+
setEnv('PRESENT', 'value');
|
|
172
|
+
setEnv('EMPTY', '');
|
|
173
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({
|
|
174
|
+
PRESENT: 'value'
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
it('should ignore empty entries produced by trailing or repeated commas', function () {
|
|
178
|
+
process.env.NAVY_HTTP_PROXY_ENV = ',FOO,,';
|
|
179
|
+
setEnv('FOO', 'value');
|
|
180
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnvAllowlist)()).to.eql({
|
|
181
|
+
FOO: 'value'
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('resolveProxyEnv', function () {
|
|
186
|
+
const namesSetDuringTest = [];
|
|
187
|
+
function setEnv(name, value) {
|
|
188
|
+
namesSetDuringTest.push(name);
|
|
189
|
+
process.env[name] = value;
|
|
190
|
+
}
|
|
191
|
+
afterEach(function () {
|
|
192
|
+
delete process.env.NAVY_HTTP_PROXY_ENV;
|
|
193
|
+
while (namesSetDuringTest.length > 0) {
|
|
194
|
+
delete process.env[namesSetDuringTest.pop()];
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
it('should return null when both sources are empty', function () {
|
|
198
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnv)()).to.equal(null);
|
|
199
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnv)({})).to.equal(null);
|
|
200
|
+
});
|
|
201
|
+
it('should return only the navyFile entries when no allowlist is set', function () {
|
|
202
|
+
const navyFile = {
|
|
203
|
+
httpProxyEnv: {
|
|
204
|
+
FOO: 'bar'
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnv)(navyFile)).to.eql({
|
|
208
|
+
FOO: 'bar'
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
it('should return only the allowlisted entries when navyFile has none', function () {
|
|
212
|
+
process.env.NAVY_HTTP_PROXY_ENV = 'FORWARD_ME';
|
|
213
|
+
setEnv('FORWARD_ME', 'hello');
|
|
214
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnv)()).to.eql({
|
|
215
|
+
FORWARD_ME: 'hello'
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
it('should merge entries from both sources', function () {
|
|
219
|
+
process.env.NAVY_HTTP_PROXY_ENV = 'FROM_ENV';
|
|
220
|
+
setEnv('FROM_ENV', 'env-value');
|
|
221
|
+
const navyFile = {
|
|
222
|
+
httpProxyEnv: {
|
|
223
|
+
FROM_FILE: 'file-value'
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnv)(navyFile)).to.eql({
|
|
227
|
+
FROM_FILE: 'file-value',
|
|
228
|
+
FROM_ENV: 'env-value'
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
it('should give the allowlist precedence on key collisions', function () {
|
|
232
|
+
process.env.NAVY_HTTP_PROXY_ENV = 'SHARED';
|
|
233
|
+
setEnv('SHARED', 'env-value');
|
|
234
|
+
const navyFile = {
|
|
235
|
+
httpProxyEnv: {
|
|
236
|
+
SHARED: 'file-value'
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
(0, _chai.expect)((0, _httpProxy.resolveProxyEnv)(navyFile)).to.eql({
|
|
240
|
+
SHARED: 'env-value'
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
65
244
|
describe('resolveDockerSocketPath', function () {
|
|
66
245
|
let originalDockerHost;
|
|
67
246
|
beforeEach(function () {
|
|
@@ -211,4 +390,35 @@ describe('reconfigureHTTPProxy', function () {
|
|
|
211
390
|
const rmCall = execStub.getCalls().find(c => Array.isArray(c.args[1]) && c.args[1].includes('rm'));
|
|
212
391
|
(0, _chai.expect)(rmCall).to.equal(undefined);
|
|
213
392
|
});
|
|
393
|
+
it('should omit the environment block from the proxy compose service when no env config is provided', async function () {
|
|
394
|
+
listNetworksStub.resolves([]);
|
|
395
|
+
await (0, _httpProxy.reconfigureHTTPProxy)({
|
|
396
|
+
navies: []
|
|
397
|
+
});
|
|
398
|
+
const written = _jsYaml.default.load(writeFileSyncStub.firstCall.args[1]);
|
|
399
|
+
(0, _chai.expect)(written.services['nginx-proxy']).to.not.have.property('environment');
|
|
400
|
+
});
|
|
401
|
+
it('should include the merged environment block on the proxy compose service when httpProxyEnv or NAVY_HTTP_PROXY_ENV is set', async function () {
|
|
402
|
+
listNetworksStub.resolves([]);
|
|
403
|
+
process.env.NAVY_HTTP_PROXY_ENV = 'FROM_ENV';
|
|
404
|
+
process.env.FROM_ENV = 'env-value';
|
|
405
|
+
try {
|
|
406
|
+
await (0, _httpProxy.reconfigureHTTPProxy)({
|
|
407
|
+
navies: [],
|
|
408
|
+
navyFile: {
|
|
409
|
+
httpProxyEnv: {
|
|
410
|
+
FROM_FILE: 'file-value'
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
const written = _jsYaml.default.load(writeFileSyncStub.firstCall.args[1]);
|
|
415
|
+
(0, _chai.expect)(written.services['nginx-proxy'].environment).to.eql({
|
|
416
|
+
FROM_FILE: 'file-value',
|
|
417
|
+
FROM_ENV: 'env-value'
|
|
418
|
+
});
|
|
419
|
+
} finally {
|
|
420
|
+
delete process.env.NAVY_HTTP_PROXY_ENV;
|
|
421
|
+
delete process.env.FROM_ENV;
|
|
422
|
+
}
|
|
423
|
+
});
|
|
214
424
|
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _chai = require("chai");
|
|
4
|
+
var _commander = require("commander");
|
|
5
|
+
/* eslint-env mocha */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* These tests check that the commander configuration the navy CLI relies on
|
|
9
|
+
* (positional options + per-subcommand option declarations) rejects options
|
|
10
|
+
* that a subcommand does not declare, using the exact error format commander
|
|
11
|
+
* emits by default.
|
|
12
|
+
*
|
|
13
|
+
* We build a minimal program with the same shape as packages/navy/src/cli/program.js
|
|
14
|
+
* rather than loading that module, because importing program.js has side effects
|
|
15
|
+
* (config reading, action wiring) that are not relevant to argument parsing.
|
|
16
|
+
*/
|
|
17
|
+
describe('cli/program unknown option handling', function () {
|
|
18
|
+
function buildProgram() {
|
|
19
|
+
const program = new _commander.Command();
|
|
20
|
+
program.exitOverride();
|
|
21
|
+
program.configureOutput({
|
|
22
|
+
writeErr: () => {},
|
|
23
|
+
writeOut: () => {}
|
|
24
|
+
});
|
|
25
|
+
program.enablePositionalOptions();
|
|
26
|
+
program.option('-e, --navy [env]', 'set the navy name to be used', 'dev');
|
|
27
|
+
program.command('status').option('--json', 'output JSON instead of a table').action(() => {});
|
|
28
|
+
program.command('start [services...]').option('-e, --navy [env]', 'set the navy name to be used', 'dev').action(() => {});
|
|
29
|
+
return program;
|
|
30
|
+
}
|
|
31
|
+
describe('a subcommand that does not declare the option', function () {
|
|
32
|
+
it('should throw a commander error for an unknown short option', function () {
|
|
33
|
+
const program = buildProgram();
|
|
34
|
+
(0, _chai.expect)(() => program.parse(['node', 'navy', 'status', '-e', 'foo'])).to.throw(/unknown option '-e'/);
|
|
35
|
+
});
|
|
36
|
+
it('should produce a non-zero exit code on the thrown error', function () {
|
|
37
|
+
const program = buildProgram();
|
|
38
|
+
try {
|
|
39
|
+
program.parse(['node', 'navy', 'status', '-e', 'foo']);
|
|
40
|
+
_chai.expect.fail('expected parse to throw');
|
|
41
|
+
} catch (ex) {
|
|
42
|
+
(0, _chai.expect)(ex.exitCode).to.be.greaterThan(0);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
it('should emit the canonical commander unknown-option message', function () {
|
|
46
|
+
const program = buildProgram();
|
|
47
|
+
try {
|
|
48
|
+
program.parse(['node', 'navy', 'status', '-e', 'foo']);
|
|
49
|
+
_chai.expect.fail('expected parse to throw');
|
|
50
|
+
} catch (ex) {
|
|
51
|
+
(0, _chai.expect)(ex.message).to.equal("error: unknown option '-e'");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
it('should still accept declared options on the same subcommand', function () {
|
|
55
|
+
const program = buildProgram();
|
|
56
|
+
(0, _chai.expect)(() => program.parse(['node', 'navy', 'status', '--json'])).to.not.throw();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('a subcommand that declares the option', function () {
|
|
60
|
+
it('should accept the option after the subcommand name', function () {
|
|
61
|
+
const program = buildProgram();
|
|
62
|
+
(0, _chai.expect)(() => program.parse(['node', 'navy', 'start', '-e', 'foo'])).to.not.throw();
|
|
63
|
+
});
|
|
64
|
+
it('should accept the option before the subcommand name (parent inherits)', function () {
|
|
65
|
+
const program = buildProgram();
|
|
66
|
+
(0, _chai.expect)(() => program.parse(['node', 'navy', '-e', 'foo', 'start'])).to.not.throw();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -28,6 +28,7 @@ describe('cli/program', function () {
|
|
|
28
28
|
helpHandlersByCommand = {};
|
|
29
29
|
fakeProgram = {
|
|
30
30
|
option: sandbox.stub().returnsThis(),
|
|
31
|
+
enablePositionalOptions: sandbox.stub().returnsThis(),
|
|
31
32
|
command(spec) {
|
|
32
33
|
const name = spec.split(' ')[0];
|
|
33
34
|
const cmd = {
|
|
@@ -155,6 +156,10 @@ describe('cli/program', function () {
|
|
|
155
156
|
loadModule();
|
|
156
157
|
(0, _chai.expect)(fakeProgram.option.calledWith('-e, --navy [env]', _sinon.default.match.string, 'dev')).to.equal(true);
|
|
157
158
|
});
|
|
159
|
+
it('should enable positional options so unknown subcommand options are rejected', function () {
|
|
160
|
+
loadModule();
|
|
161
|
+
(0, _chai.expect)(fakeProgram.enablePositionalOptions.calledOnce).to.equal(true);
|
|
162
|
+
});
|
|
158
163
|
});
|
|
159
164
|
describe('lazyRequire wrapped action', function () {
|
|
160
165
|
it('should pass through arguments to the underlying module', async function () {
|
|
@@ -324,6 +329,103 @@ describe('cli/program', function () {
|
|
|
324
329
|
// exercised in practice. We do not test it because asserting that
|
|
325
330
|
// behaviour would lock in a latent bug.
|
|
326
331
|
});
|
|
332
|
+
describe('normaliseLazyRequireArgs (via lazyRequire wrapped action)', function () {
|
|
333
|
+
let importSpy;
|
|
334
|
+
function loadWithImportSpy() {
|
|
335
|
+
importSpy = sandbox.stub().resolves('imported');
|
|
336
|
+
return _proxyquire.default.noCallThru()('../program', {
|
|
337
|
+
commander: {
|
|
338
|
+
program: createCapturingProgram()
|
|
339
|
+
},
|
|
340
|
+
'../errors': {
|
|
341
|
+
NavyError: _errors.NavyError
|
|
342
|
+
},
|
|
343
|
+
'../config': {
|
|
344
|
+
getConfig: getConfigStub
|
|
345
|
+
},
|
|
346
|
+
'../driver-logging': {
|
|
347
|
+
startDriverLogging: startDriverLoggingStub,
|
|
348
|
+
stopDriverLogging: stopDriverLoggingStub
|
|
349
|
+
},
|
|
350
|
+
'../config-provider': {
|
|
351
|
+
getImportCommandLineOptions: getImportCommandLineOptionsStub
|
|
352
|
+
},
|
|
353
|
+
'../navy': {
|
|
354
|
+
getNavy: getNavyStub
|
|
355
|
+
},
|
|
356
|
+
'./import': importSpy,
|
|
357
|
+
'./launch': () => Promise.resolve(),
|
|
358
|
+
'./ps': () => Promise.resolve(),
|
|
359
|
+
'./updates': () => Promise.resolve(),
|
|
360
|
+
'./logs': () => Promise.resolve(),
|
|
361
|
+
'./health': () => Promise.resolve(),
|
|
362
|
+
'./wait-for-healthy': () => Promise.resolve(),
|
|
363
|
+
'./https': () => Promise.resolve(),
|
|
364
|
+
'./open': () => Promise.resolve(),
|
|
365
|
+
'./develop': () => Promise.resolve(),
|
|
366
|
+
'./live': () => Promise.resolve(),
|
|
367
|
+
'./run': () => Promise.resolve(),
|
|
368
|
+
'./refresh-config': () => Promise.resolve(),
|
|
369
|
+
'./status': () => Promise.resolve(),
|
|
370
|
+
'./doctor': () => Promise.resolve(),
|
|
371
|
+
'./config/wrapper': () => Promise.resolve(),
|
|
372
|
+
'./external-ip': () => Promise.resolve(),
|
|
373
|
+
'./lan-ip': () => Promise.resolve(),
|
|
374
|
+
'./local-ip': () => Promise.resolve()
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
it('should pass through unchanged when invoked with no arguments at all', async function () {
|
|
378
|
+
loadWithImportSpy();
|
|
379
|
+
capturedActions.import();
|
|
380
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
381
|
+
(0, _chai.expect)(importSpy.calledOnce).to.equal(true);
|
|
382
|
+
(0, _chai.expect)(importSpy.firstCall.args).to.eql([]);
|
|
383
|
+
});
|
|
384
|
+
it('should strip the trailing Command instance when no other args were supplied', async function () {
|
|
385
|
+
loadWithImportSpy();
|
|
386
|
+
const fakeCommand = {
|
|
387
|
+
optsWithGlobals: () => ({
|
|
388
|
+
navy: 'global'
|
|
389
|
+
})
|
|
390
|
+
};
|
|
391
|
+
capturedActions.import(fakeCommand);
|
|
392
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
393
|
+
(0, _chai.expect)(importSpy.calledOnce).to.equal(true);
|
|
394
|
+
(0, _chai.expect)(importSpy.firstCall.args).to.eql([]);
|
|
395
|
+
});
|
|
396
|
+
it('should merge inherited globals into the trailing options object before forwarding', async function () {
|
|
397
|
+
loadWithImportSpy();
|
|
398
|
+
const fakeCommand = {
|
|
399
|
+
optsWithGlobals: () => ({
|
|
400
|
+
navy: 'from-global',
|
|
401
|
+
extra: 1
|
|
402
|
+
}),
|
|
403
|
+
getOptionValueSource: () => 'default'
|
|
404
|
+
};
|
|
405
|
+
capturedActions.import({
|
|
406
|
+
navy: 'subcommand-default'
|
|
407
|
+
}, fakeCommand);
|
|
408
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
409
|
+
(0, _chai.expect)(importSpy.calledOnce).to.equal(true);
|
|
410
|
+
(0, _chai.expect)(importSpy.firstCall.args).to.have.lengthOf(1);
|
|
411
|
+
(0, _chai.expect)(importSpy.firstCall.args[0]).to.eql({
|
|
412
|
+
navy: 'from-global',
|
|
413
|
+
extra: 1
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
it('should leave non-object trailing arguments untouched when stripping the command', async function () {
|
|
417
|
+
loadWithImportSpy();
|
|
418
|
+
const fakeCommand = {
|
|
419
|
+
optsWithGlobals: () => ({
|
|
420
|
+
navy: 'global'
|
|
421
|
+
})
|
|
422
|
+
};
|
|
423
|
+
capturedActions.import(['web', 'api'], fakeCommand);
|
|
424
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
425
|
+
(0, _chai.expect)(importSpy.calledOnce).to.equal(true);
|
|
426
|
+
(0, _chai.expect)(importSpy.firstCall.args).to.eql([['web', 'api']]);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
327
429
|
describe('basicCliWrapper action', function () {
|
|
328
430
|
beforeEach(function () {
|
|
329
431
|
loadModule();
|
|
@@ -349,6 +451,18 @@ describe('cli/program', function () {
|
|
|
349
451
|
}, fakeCommand);
|
|
350
452
|
(0, _chai.expect)(getNavyStub.firstCall.args[0]).to.equal('from-global');
|
|
351
453
|
});
|
|
454
|
+
it('should fall back to maybeServices as opts when only an opts object and command are passed', async function () {
|
|
455
|
+
navyStub.getAvailableServiceNames = sandbox.stub().resolves([]);
|
|
456
|
+
const fakeCommand = {
|
|
457
|
+
optsWithGlobals: () => ({
|
|
458
|
+
navy: 'from-global'
|
|
459
|
+
})
|
|
460
|
+
};
|
|
461
|
+
await actionsByCommand['available-services']({
|
|
462
|
+
navy: 'subcommand-default'
|
|
463
|
+
}, fakeCommand);
|
|
464
|
+
(0, _chai.expect)(getNavyStub.firstCall.args[0]).to.equal('from-global');
|
|
465
|
+
});
|
|
352
466
|
it('should pass undefined when called with an empty service list', async function () {
|
|
353
467
|
navyStub.start = sandbox.stub().resolves();
|
|
354
468
|
await actionsByCommand.start([], {
|
|
@@ -454,6 +568,7 @@ describe('cli/program', function () {
|
|
|
454
568
|
capturedActions = {};
|
|
455
569
|
return {
|
|
456
570
|
option: sandbox.stub().returnsThis(),
|
|
571
|
+
enablePositionalOptions: sandbox.stub().returnsThis(),
|
|
457
572
|
command(spec) {
|
|
458
573
|
const name = spec.split(' ')[0];
|
|
459
574
|
const cmd = {
|
package/lib/cli/program.js
CHANGED
|
@@ -135,6 +135,11 @@ function lazyRequire(path) {
|
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
const defaultNavy = process.env.NAVY_NAME || (0, _config.getConfig)().defaultNavy;
|
|
138
|
+
|
|
139
|
+
// Strictly separate parent and subcommand options so an option that isn't
|
|
140
|
+
// declared on a subcommand (e.g. `navy status -e foo`) is reported as
|
|
141
|
+
// unknown rather than silently absorbed by the parent program.
|
|
142
|
+
_commander.program.enablePositionalOptions();
|
|
138
143
|
_commander.program.option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy);
|
|
139
144
|
const importCommand = _commander.program.command('import').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Imports docker compose configuration from the current working directory and initialises a new navy').action(lazyRequire('./import'));
|
|
140
145
|
(0, _configProvider.getImportCommandLineOptions)().forEach(opt => importCommand.option(...opt));
|
|
@@ -44,4 +44,28 @@ describe('cli/util/merge-action-options', function () {
|
|
|
44
44
|
});
|
|
45
45
|
(0, _chai.expect)(spy.calledOnce).to.equal(true);
|
|
46
46
|
});
|
|
47
|
+
it('should let an explicit subcommand option beat the inherited parent default', function () {
|
|
48
|
+
const command = {
|
|
49
|
+
optsWithGlobals: () => ({
|
|
50
|
+
navy: 'parent-default'
|
|
51
|
+
}),
|
|
52
|
+
getOptionValueSource: key => key === 'navy' ? 'cli' : 'default'
|
|
53
|
+
};
|
|
54
|
+
const merged = (0, _mergeActionOptions.mergeActionOptions)({
|
|
55
|
+
navy: 'from-subcommand'
|
|
56
|
+
}, command);
|
|
57
|
+
(0, _chai.expect)(merged.navy).to.equal('from-subcommand');
|
|
58
|
+
});
|
|
59
|
+
it('should keep the parent value when the subcommand value came from its default', function () {
|
|
60
|
+
const command = {
|
|
61
|
+
optsWithGlobals: () => ({
|
|
62
|
+
navy: 'from-parent'
|
|
63
|
+
}),
|
|
64
|
+
getOptionValueSource: () => 'default'
|
|
65
|
+
};
|
|
66
|
+
const merged = (0, _mergeActionOptions.mergeActionOptions)({
|
|
67
|
+
navy: 'subcommand-default'
|
|
68
|
+
}, command);
|
|
69
|
+
(0, _chai.expect)(merged.navy).to.equal('from-parent');
|
|
70
|
+
});
|
|
47
71
|
});
|
|
@@ -6,15 +6,26 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.mergeActionOptions = mergeActionOptions;
|
|
7
7
|
/**
|
|
8
8
|
* Merge Commander's per-command opts with inherited globals (e.g. `navy -e dev ps`).
|
|
9
|
-
* `optsWithGlobals()`
|
|
10
|
-
*
|
|
9
|
+
* `optsWithGlobals()` has "globals overwrite locals" semantics, which makes parent
|
|
10
|
+
* defaults win over child defaults. With positional options enabled, however, a
|
|
11
|
+
* value supplied after the subcommand name (e.g. `navy ps -e dev`) is parsed onto
|
|
12
|
+
* the subcommand, not the parent, so we re-apply any option whose source on the
|
|
13
|
+
* subcommand is non-default to keep the explicit value.
|
|
11
14
|
*/
|
|
12
15
|
function mergeActionOptions(parsedOpts, command) {
|
|
13
16
|
if (!command || typeof command.optsWithGlobals !== 'function') {
|
|
14
17
|
return parsedOpts;
|
|
15
18
|
}
|
|
16
|
-
|
|
17
|
-
...parsedOpts,
|
|
19
|
+
const merged = {
|
|
18
20
|
...command.optsWithGlobals()
|
|
19
21
|
};
|
|
22
|
+
if (typeof command.getOptionValueSource === 'function') {
|
|
23
|
+
for (const key of Object.keys(parsedOpts)) {
|
|
24
|
+
const source = command.getOptionValueSource(key);
|
|
25
|
+
if (source && source !== 'default') {
|
|
26
|
+
merged[key] = parsedOpts[key];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return merged;
|
|
20
31
|
}
|
package/lib/http-proxy.js
CHANGED
|
@@ -6,6 +6,9 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
});
|
|
7
7
|
exports.reconfigureHTTPProxy = reconfigureHTTPProxy;
|
|
8
8
|
exports.resolveDockerSocketPath = resolveDockerSocketPath;
|
|
9
|
+
exports.resolveProxyEnv = resolveProxyEnv;
|
|
10
|
+
exports.resolveProxyEnvAllowlist = resolveProxyEnvAllowlist;
|
|
11
|
+
exports.resolveProxyEnvFromNavyFile = resolveProxyEnvFromNavyFile;
|
|
9
12
|
exports.resolveProxyImage = resolveProxyImage;
|
|
10
13
|
var _os = _interopRequireDefault(require("os"));
|
|
11
14
|
var _path = _interopRequireDefault(require("path"));
|
|
@@ -21,6 +24,43 @@ const DEFAULT_DOCKER_SOCKET = '/var/run/docker.sock';
|
|
|
21
24
|
function resolveProxyImage(navyFile) {
|
|
22
25
|
return process.env.NAVY_HTTP_PROXY_IMAGE || navyFile && navyFile.httpProxyImage || DEFAULT_PROXY_IMAGE;
|
|
23
26
|
}
|
|
27
|
+
function resolveProxyEnvFromNavyFile(navyFile) {
|
|
28
|
+
if (!navyFile) return {};
|
|
29
|
+
const raw = navyFile.httpProxyEnv;
|
|
30
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
31
|
+
const result = {};
|
|
32
|
+
for (const key of Object.keys(raw)) {
|
|
33
|
+
const value = raw[key];
|
|
34
|
+
if (value === null || value === undefined) continue;
|
|
35
|
+
const coerced = String(value);
|
|
36
|
+
if (coerced === '') continue;
|
|
37
|
+
result[key] = coerced;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function resolveProxyEnvAllowlist() {
|
|
42
|
+
const allowlist = process.env.NAVY_HTTP_PROXY_ENV;
|
|
43
|
+
if (!allowlist) return {};
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const result = {};
|
|
46
|
+
for (const rawName of allowlist.split(',')) {
|
|
47
|
+
const name = rawName.trim();
|
|
48
|
+
if (!name || seen.has(name)) continue;
|
|
49
|
+
seen.add(name);
|
|
50
|
+
const value = process.env[name];
|
|
51
|
+
if (typeof value !== 'string' || value === '') continue;
|
|
52
|
+
result[name] = value;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
function resolveProxyEnv(navyFile) {
|
|
57
|
+
const merged = {
|
|
58
|
+
...resolveProxyEnvFromNavyFile(navyFile),
|
|
59
|
+
...resolveProxyEnvAllowlist()
|
|
60
|
+
};
|
|
61
|
+
if (Object.keys(merged).length === 0) return null;
|
|
62
|
+
return merged;
|
|
63
|
+
}
|
|
24
64
|
|
|
25
65
|
// Resolve the host path to the Docker socket. The proxy container needs to
|
|
26
66
|
// read events from the same daemon as the rest of navy, so we honour
|
|
@@ -61,6 +101,7 @@ async function updateComposeConfig(navies, navyFile) {
|
|
|
61
101
|
volumes.push(`${certsPath}:/etc/nginx/certs`);
|
|
62
102
|
volumes.push(`${certsPath}:/etc/nginx/dhparam`); // to persist DH params
|
|
63
103
|
}
|
|
104
|
+
const proxyEnv = resolveProxyEnv(navyFile);
|
|
64
105
|
const config = {
|
|
65
106
|
version: '2',
|
|
66
107
|
services: {
|
|
@@ -69,6 +110,9 @@ async function updateComposeConfig(navies, navyFile) {
|
|
|
69
110
|
ports,
|
|
70
111
|
networks: networks.map(net => net.Name),
|
|
71
112
|
volumes,
|
|
113
|
+
...(proxyEnv ? {
|
|
114
|
+
environment: proxyEnv
|
|
115
|
+
} : {}),
|
|
72
116
|
restart: 'always'
|
|
73
117
|
}
|
|
74
118
|
},
|