navy 7.0.0-alpha.3 → 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.
@@ -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
  });
@@ -329,6 +329,103 @@ describe('cli/program', function () {
329
329
  // exercised in practice. We do not test it because asserting that
330
330
  // behaviour would lock in a latent bug.
331
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
+ });
332
429
  describe('basicCliWrapper action', function () {
333
430
  beforeEach(function () {
334
431
  loadModule();
@@ -354,6 +451,18 @@ describe('cli/program', function () {
354
451
  }, fakeCommand);
355
452
  (0, _chai.expect)(getNavyStub.firstCall.args[0]).to.equal('from-global');
356
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
+ });
357
466
  it('should pass undefined when called with an empty service list', async function () {
358
467
  navyStub.start = sandbox.stub().resolves();
359
468
  await actionsByCommand.start([], {
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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "navy",
3
- "version": "7.0.0-alpha.3",
3
+ "version": "7.0.0-alpha.4",
4
4
  "description": "Quick and powerful development environments using Docker and Docker Compose",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",