Skip to content

Commit c02c5c1

Browse files
authored
feat: implement HADRON_ISOLATED through preferences COMPASS-6065 (#3503)
- Implement `HADRON_ISOLATED` through the `networkTraffic` preference. - Derive the value of other preferences that are shadowed by `networkTraffic` through a generic mechanism for doing so (we will also need this for `readOnly`/`enableShell`) - Add an e2e test that runs on Linux and verifies via `strace` that Compass does not actually perform external network I/O when `--no-network-traffic` has been specified.
1 parent 3bdf884 commit c02c5c1

File tree

21 files changed

+446
-132
lines changed

21 files changed

+446
-132
lines changed

package-lock.json

Lines changed: 0 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-e2e-tests/helpers/compass.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,9 @@ export class Compass {
273273

274274
interface StartCompassOptions {
275275
firstRun?: boolean;
276+
noCloseSettingsModal?: boolean;
276277
extraSpawnArgs?: string[];
278+
wrapBinary?: (binary: string) => Promise<string> | string;
277279
}
278280

279281
let defaultUserDataDir: string | undefined;
@@ -426,6 +428,8 @@ async function startCompass(opts: StartCompassOptions = {}): Promise<Compass> {
426428
: 100,
427429
};
428430

431+
const maybeWrappedBinary = (await opts.wrapBinary?.(binary)) ?? binary;
432+
429433
process.env.APP_ENV = 'webdriverio';
430434
process.env.DEBUG = `${process.env.DEBUG ?? ''},mongodb-compass:main:logging`;
431435
process.env.MONGODB_COMPASS_TEST_LOG_DIR = path.join(LOG_PATH, 'app');
@@ -436,7 +440,7 @@ async function startCompass(opts: StartCompassOptions = {}): Promise<Compass> {
436440
browserName: 'chrome',
437441
// https://chromedriver.chromium.org/capabilities#h.p_ID_106
438442
'goog:chromeOptions': {
439-
binary,
443+
binary: maybeWrappedBinary,
440444
args: chromeArgs,
441445
},
442446
// more chrome options
@@ -655,14 +659,14 @@ function augmentError(error: Error, stack: string) {
655659
}
656660

657661
export async function beforeTests(
658-
opts?: StartCompassOptions
662+
opts: StartCompassOptions = {}
659663
): Promise<Compass> {
660664
const compass = await startCompass(opts);
661665

662666
const { browser } = compass;
663667

664668
await browser.waitForConnectionScreen();
665-
if (compass.isFirstRun) {
669+
if (compass.isFirstRun && !opts.noCloseSettingsModal) {
666670
await browser.closeSettingsModal();
667671
}
668672

packages/compass-e2e-tests/tests/global-preferences.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,28 @@ describe('Global preferences', function () {
146146
}
147147
});
148148

149+
it('allows setting networkTraffic: false and reflects that in the settings modal', async function () {
150+
await fs.writeFile(path.join(tmpdir, 'config'), 'networkTraffic: false\n');
151+
// No settings modal opened for Isolated edition
152+
const compass = await beforeTests({ noCloseSettingsModal: true });
153+
try {
154+
const browser = compass.browser;
155+
await browser.openSettingsModal();
156+
const { disabled, value, bannerText } = await getCheckboxAndBannerState(
157+
browser,
158+
'enableMaps'
159+
);
160+
expect(value).to.equal('false');
161+
expect(disabled).to.equal(''); // null = missing attribute, '' = set
162+
expect(bannerText).to.include(
163+
'This setting cannot be modified as it has been set in the global Compass configuration file.'
164+
);
165+
} finally {
166+
await afterTest(compass, this.currentTest);
167+
await afterTests(compass, this.currentTest);
168+
}
169+
});
170+
149171
it('allows showing version information', async function () {
150172
const { stdout } = await runCompassOnce(['--version']);
151173
expect(stdout.trim()).to.match(/^MongoDB Compass.*\d$/);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { beforeTests, afterTests } from '../helpers/compass';
2+
import { promises as fs } from 'fs';
3+
import path from 'path';
4+
import os from 'os';
5+
6+
describe('networkTraffic: false / Isolated Edition', function () {
7+
let tmpdir: string;
8+
let i = 0;
9+
10+
before(function () {
11+
if (process.platform !== 'linux') {
12+
// No strace on other platforms
13+
return this.skip();
14+
}
15+
});
16+
17+
beforeEach(async function () {
18+
tmpdir = path.join(
19+
os.tmpdir(),
20+
`compass-global-preferences-${Date.now().toString(32)}-${++i}`
21+
);
22+
await fs.mkdir(tmpdir, { recursive: true });
23+
});
24+
25+
afterEach(async function () {
26+
await fs.rmdir(tmpdir, { recursive: true });
27+
});
28+
29+
it('does not attempt to perform network I/O', async function () {
30+
// This is a bit of an oddball test. On Linux, we run Compass under strace(1),
31+
// tracing all connect(2) calls made by Compass and its child processes.
32+
// We expect a connection to the database, but otherwise no outgoing network I/O.
33+
34+
const outfile = path.join(tmpdir, 'strace-out.log');
35+
async function wrapBinary(binary: string): Promise<string> {
36+
const wrapperFile = path.join(tmpdir, 'wrap.sh');
37+
await fs.writeFile(
38+
wrapperFile,
39+
`#!/bin/bash\nulimit -c 0; exec strace -f -e connect -qqq -o '${outfile}' '${binary}' "$@"\n`
40+
);
41+
await fs.chmod(wrapperFile, 0o755);
42+
return wrapperFile;
43+
}
44+
45+
const compass = await beforeTests({
46+
extraSpawnArgs: ['--no-network-traffic'],
47+
noCloseSettingsModal: true,
48+
wrapBinary,
49+
});
50+
const browser = compass.browser;
51+
52+
{
53+
// TODO: Remove this once we are including https://github.com/mongodb-js/mongosh/pull/1349
54+
const exitOnDisconnectFile = path.join(tmpdir, 'exitOnDisconnect.js');
55+
await fs.writeFile(
56+
exitOnDisconnectFile,
57+
'process.once("disconnect", () => process.exit())'
58+
);
59+
await browser.execute((exitOnDisconnectFile) => {
60+
process.env.NODE_OPTIONS ??= '';
61+
process.env.NODE_OPTIONS += ` --require "${exitOnDisconnectFile}"`;
62+
}, exitOnDisconnectFile);
63+
}
64+
65+
try {
66+
await browser.connectWithConnectionString(
67+
'mongodb://localhost:27091/test'
68+
);
69+
} finally {
70+
await afterTests(compass, this.currentTest);
71+
}
72+
73+
const straceLog = await fs.readFile(outfile, 'utf8');
74+
const connectCalls = straceLog.matchAll(/\bconnect\s*\((?<args>.*)\) =/g);
75+
const connectTargets = new Set<string>();
76+
for (const { groups } of connectCalls) {
77+
const args = groups!.args;
78+
// Possible format for the address argument in 'args':
79+
// sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"
80+
// sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("127.0.0.1")
81+
// sa_family=AF_INET6, sin6_port=htons(80), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2606:2800:220:1:248:1893:25c8:1946", &sin6_addr), sin6_scope_id=0
82+
if (!args.includes('AF_INET')) continue;
83+
const match = args.match(
84+
/sa_family=AF_INET6?.*sin6?_port=htons\((?<port>\d+)\).*inet_(addr\("(?<ipv4>[^"]+)"\)|pton\(AF_INET6,\s*"(?<ipv6>[^"]+)")/
85+
)?.groups;
86+
if (!match) {
87+
throw new Error(`Unknown traced connect() target: ${args}`);
88+
}
89+
connectTargets.add(
90+
match.ipv4
91+
? `${match.ipv4}:${match.port}`
92+
: `[${match.ipv6}]:${match.port}`
93+
);
94+
}
95+
96+
if (
97+
[...connectTargets].some(
98+
(target) => !/^127.0.0.1:|^\[::1\]:/.test(target)
99+
)
100+
) {
101+
throw new Error(`Connected to unexpected host! ${[...connectTargets]}`);
102+
}
103+
if (![...connectTargets].some((target) => /:27091$/.test(target))) {
104+
throw new Error(
105+
`Missed connection to database server in connect trace! ${[
106+
...connectTargets,
107+
]}`
108+
);
109+
}
110+
});
111+
});

packages/compass-preferences-model/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,11 @@
5757
"@mongodb-js/eslint-config-compass": "^1.0.1",
5858
"@mongodb-js/mocha-config-compass": "^1.0.1",
5959
"@types/js-yaml": "^4.0.5",
60-
"@types/lodash.pickby": "^4.6.7",
6160
"@types/yargs-parser": "21.0.0",
6261
"chai": "^4.3.6",
6362
"depcheck": "^1.4.1",
6463
"eslint": "^7.25.0",
6564
"hadron-ipc": "^3.1.0",
66-
"lodash.pickby": "^4.6.0",
6765
"mocha": "^6.0.2"
6866
}
6967
}

packages/compass-preferences-model/src/global-config.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('Global config file handling', function () {
1616
await fs.mkdir(tmpdir, { recursive: true });
1717
});
1818

19-
after(async function () {
19+
afterEach(async function () {
2020
await fs.rm(tmpdir, { recursive: true });
2121
});
2222

packages/compass-preferences-model/src/global-config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export interface GlobalPreferenceSources {
175175
export interface ParsedGlobalPreferencesResult {
176176
cli: Partial<GlobalPreferences>;
177177
global: Partial<GlobalPreferences>;
178+
hardcoded?: Partial<GlobalPreferences>;
178179
preferenceParseErrors: string[];
179180
}
180181

@@ -260,7 +261,9 @@ export function getHelpText(): string {
260261
text += formatSingleOption(key, 'cli');
261262
}
262263
text +=
263-
'\n(Options marked with (*) are also configurable through the global configuration file.)\n';
264+
'\nOptions marked with (*) are also configurable through the global configuration file.\n';
265+
text +=
266+
'Boolean options can be negated by using a `--no` prefix, e.g. `--no-networkTraffic`.\n';
264267
text += '\nOnly available in the global configuration file:\n\n';
265268
for (const key of Object.keys(
266269
allPreferencesProps

0 commit comments

Comments
 (0)