start-command 0.30.5 → 0.31.0
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/bin/cli.js +1 -4
- package/src/lib/args-parser.js +12 -2
- package/src/lib/docker-cleanup.js +17 -0
- package/src/lib/docker-network-options.js +44 -0
- package/src/lib/isolation.js +41 -4
- package/src/lib/usage.js +2 -0
- package/test/docker-network-integration.js +120 -0
- package/test/docker-runtime-options.js +75 -1
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -581,10 +581,7 @@ async function runWithIsolation(
|
|
|
581
581
|
alwaysCleanupContainer: options.alwaysCleanupContainer,
|
|
582
582
|
keepContainer: options.keepContainer,
|
|
583
583
|
keepContainerOnFail: options.keepContainerOnFail,
|
|
584
|
-
|
|
585
|
-
mounts: options.mounts,
|
|
586
|
-
env: options.env,
|
|
587
|
-
privileged: options.privileged,
|
|
584
|
+
...buildDockerRuntimeMetadata(options),
|
|
588
585
|
shell: options.shell,
|
|
589
586
|
logPath: logFilePath,
|
|
590
587
|
});
|
package/src/lib/args-parser.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* --mount <mount-spec> Docker --mount spec (repeatable, docker only)
|
|
16
16
|
* --env, -e <KEY=VALUE> Environment variable for docker container (repeatable, docker only)
|
|
17
17
|
* --privileged Run docker container in privileged mode (docker only)
|
|
18
|
+
* --network <name> Connect docker container to a named network (docker only)
|
|
19
|
+
* --network-alias <alias> Add a network-scoped alias (repeatable, docker only)
|
|
18
20
|
* --endpoint <endpoint> SSH endpoint (required for ssh isolation, e.g., user@host)
|
|
19
21
|
* --isolated-user, -u [username] Create isolated user with same permissions (auto-generated name if not specified)
|
|
20
22
|
* --keep-user Keep isolated user after command completes (don't delete)
|
|
@@ -37,6 +39,7 @@
|
|
|
37
39
|
*/
|
|
38
40
|
|
|
39
41
|
const { getDefaultDockerImage } = require('./docker-utils');
|
|
42
|
+
const dockerNetworkOptions = require('./docker-network-options');
|
|
40
43
|
const { parseSequence, isSequence } = require('./sequence-parser');
|
|
41
44
|
|
|
42
45
|
// Debug mode from environment
|
|
@@ -178,6 +181,8 @@ function parseArgs(args) {
|
|
|
178
181
|
mounts: [], // Docker --mount specs, applied to docker levels
|
|
179
182
|
env: [], // Docker environment variables (-e/--env, KEY=VALUE), applied to docker levels
|
|
180
183
|
privileged: false, // Run docker container in privileged mode
|
|
184
|
+
network: null, // Docker network name
|
|
185
|
+
networkAliases: [], // Docker network-scoped aliases
|
|
181
186
|
endpoint: null, // SSH endpoint (current level, e.g., user@host)
|
|
182
187
|
endpointStack: null, // SSH endpoints for each level (with nulls for non-ssh levels)
|
|
183
188
|
user: false, // Create isolated user
|
|
@@ -396,6 +401,11 @@ function parseOption(args, index, options) {
|
|
|
396
401
|
return 1;
|
|
397
402
|
}
|
|
398
403
|
|
|
404
|
+
const networkOption = dockerNetworkOptions.parse(args, index, options);
|
|
405
|
+
if (networkOption) {
|
|
406
|
+
return networkOption;
|
|
407
|
+
}
|
|
408
|
+
|
|
399
409
|
// --endpoint (for ssh) - supports sequence for stacked isolation
|
|
400
410
|
if (arg === '--endpoint') {
|
|
401
411
|
if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
|
|
@@ -645,8 +655,7 @@ function parseOption(args, index, options) {
|
|
|
645
655
|
}
|
|
646
656
|
|
|
647
657
|
/**
|
|
648
|
-
* Throw if docker runtime options
|
|
649
|
-
* are present but the isolation configuration does not include docker.
|
|
658
|
+
* Throw if docker runtime options are present without docker isolation.
|
|
650
659
|
* @param {object} options - Parsed options
|
|
651
660
|
* @throws {Error} If a docker-only option is set without docker isolation
|
|
652
661
|
*/
|
|
@@ -671,6 +680,7 @@ function validateDockerRuntimeOptionsRequireDocker(options) {
|
|
|
671
680
|
'--privileged option is only valid when isolation stack includes docker'
|
|
672
681
|
);
|
|
673
682
|
}
|
|
683
|
+
dockerNetworkOptions.validateRequireDocker(options);
|
|
674
684
|
}
|
|
675
685
|
|
|
676
686
|
function validateDockerCleanupOptions(options, hasDocker) {
|
|
@@ -109,6 +109,22 @@ function readDockerContainerOomKilled(containerName) {
|
|
|
109
109
|
return null;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function readDockerContainerStatus(containerName) {
|
|
113
|
+
const result = spawnSync(
|
|
114
|
+
getDockerCommand(),
|
|
115
|
+
['inspect', '-f', '{{.State.Status}}', containerName],
|
|
116
|
+
getDockerSpawnOptions({
|
|
117
|
+
encoding: 'utf8',
|
|
118
|
+
env: process.env,
|
|
119
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
if (result.error || result.status !== 0) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return String(result.stdout || '').trim() || null;
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
function removeDockerContainer(containerName, logPath = null) {
|
|
113
129
|
const result = spawnSync(
|
|
114
130
|
getDockerCommand(),
|
|
@@ -237,6 +253,7 @@ module.exports = {
|
|
|
237
253
|
getDockerContainerCleanupInstructions,
|
|
238
254
|
appendDockerContainerCleanupPolicyMessage,
|
|
239
255
|
readDockerContainerOomKilled,
|
|
256
|
+
readDockerContainerStatus,
|
|
240
257
|
removeDockerContainer,
|
|
241
258
|
buildDetachedDockerCompletionScript,
|
|
242
259
|
startDetachedDockerCompletionWatcher,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Parse and validate Docker network wrapper options. */
|
|
2
|
+
|
|
3
|
+
function parseDockerNetworkOption(args, index, options) {
|
|
4
|
+
const arg = args[index];
|
|
5
|
+
if (arg === '--network' || arg === '--network-alias') {
|
|
6
|
+
if (index + 1 >= args.length || args[index + 1].startsWith('-')) {
|
|
7
|
+
const value = arg === '--network' ? 'network name' : 'alias';
|
|
8
|
+
throw new Error(`Option ${arg} requires a ${value} argument`);
|
|
9
|
+
}
|
|
10
|
+
if (arg === '--network') {
|
|
11
|
+
options.network = args[index + 1];
|
|
12
|
+
} else {
|
|
13
|
+
options.networkAliases.push(args[index + 1]);
|
|
14
|
+
}
|
|
15
|
+
return 2;
|
|
16
|
+
}
|
|
17
|
+
if (arg.startsWith('--network=')) {
|
|
18
|
+
options.network = arg.slice('--network='.length);
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
if (arg.startsWith('--network-alias=')) {
|
|
22
|
+
options.networkAliases.push(arg.slice('--network-alias='.length));
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateDockerNetworkOptionsRequireDocker(options) {
|
|
29
|
+
if (options.network) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'--network option is only valid when isolation stack includes docker'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (options.networkAliases?.length > 0) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'--network-alias option is only valid when isolation stack includes docker'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
parse: parseDockerNetworkOption,
|
|
43
|
+
validateRequireDocker: validateDockerNetworkOptionsRequireDocker,
|
|
44
|
+
};
|
package/src/lib/isolation.js
CHANGED
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
getDockerContainerCleanupInstructions,
|
|
29
29
|
appendDockerContainerCleanupPolicyMessage,
|
|
30
30
|
readDockerContainerOomKilled,
|
|
31
|
+
readDockerContainerStatus,
|
|
31
32
|
removeDockerContainer,
|
|
32
33
|
startDetachedDockerCompletionWatcher,
|
|
33
34
|
spawnAttachedDocker,
|
|
@@ -499,9 +500,9 @@ const {
|
|
|
499
500
|
/**
|
|
500
501
|
* Build the docker run runtime argument list contributed by configurable
|
|
501
502
|
* container options: privileged mode, environment variables, volumes/bind
|
|
502
|
-
* mounts,
|
|
503
|
+
* mounts, --mount specs, and network configuration. Returned in a stable order so they can be spliced
|
|
503
504
|
* into the `docker run` argv before the image name.
|
|
504
|
-
* @param {object} options - Options (privileged, env, volumes, mounts)
|
|
505
|
+
* @param {object} options - Options (privileged, env, volumes, mounts, network, networkAliases)
|
|
505
506
|
* @returns {string[]} Docker CLI arguments
|
|
506
507
|
*/
|
|
507
508
|
function buildDockerRuntimeArgs(options = {}) {
|
|
@@ -518,6 +519,12 @@ function buildDockerRuntimeArgs(options = {}) {
|
|
|
518
519
|
for (const mount of options.mounts || []) {
|
|
519
520
|
args.push('--mount', mount);
|
|
520
521
|
}
|
|
522
|
+
if (options.network) {
|
|
523
|
+
args.push('--network', options.network);
|
|
524
|
+
}
|
|
525
|
+
for (const alias of options.networkAliases || []) {
|
|
526
|
+
args.push('--network-alias', alias);
|
|
527
|
+
}
|
|
521
528
|
return args;
|
|
522
529
|
}
|
|
523
530
|
|
|
@@ -542,6 +549,14 @@ function buildDockerRuntimeStatusLines(options = {}) {
|
|
|
542
549
|
if (options.privileged) {
|
|
543
550
|
lines.push(`[Isolation] Privileged: true`);
|
|
544
551
|
}
|
|
552
|
+
if (options.network) {
|
|
553
|
+
lines.push(`[Isolation] Network: ${options.network}`);
|
|
554
|
+
}
|
|
555
|
+
if (options.networkAliases && options.networkAliases.length > 0) {
|
|
556
|
+
lines.push(
|
|
557
|
+
`[Isolation] Network aliases: ${options.networkAliases.join(', ')}`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
545
560
|
return lines;
|
|
546
561
|
}
|
|
547
562
|
|
|
@@ -549,7 +564,7 @@ function buildDockerRuntimeStatusLines(options = {}) {
|
|
|
549
564
|
* Build the execution-record metadata for docker runtime options, normalizing
|
|
550
565
|
* empty collections and a falsy privileged flag to `null`.
|
|
551
566
|
* @param {object} options - Options (volumes, mounts, env, privileged)
|
|
552
|
-
* @returns {{volumes: ?string[], mounts: ?string[], env: ?string[], privileged: ?boolean}}
|
|
567
|
+
* @returns {{volumes: ?string[], mounts: ?string[], env: ?string[], privileged: ?boolean, network: ?string, networkAliases: ?string[]}}
|
|
553
568
|
*/
|
|
554
569
|
function buildDockerRuntimeMetadata(options = {}) {
|
|
555
570
|
return {
|
|
@@ -558,6 +573,11 @@ function buildDockerRuntimeMetadata(options = {}) {
|
|
|
558
573
|
mounts: options.mounts && options.mounts.length > 0 ? options.mounts : null,
|
|
559
574
|
env: options.env && options.env.length > 0 ? options.env : null,
|
|
560
575
|
privileged: options.privileged || null,
|
|
576
|
+
network: options.network || null,
|
|
577
|
+
networkAliases:
|
|
578
|
+
options.networkAliases && options.networkAliases.length > 0
|
|
579
|
+
? options.networkAliases
|
|
580
|
+
: null,
|
|
561
581
|
};
|
|
562
582
|
}
|
|
563
583
|
|
|
@@ -597,6 +617,8 @@ function runInDocker(command, options = {}) {
|
|
|
597
617
|
}
|
|
598
618
|
|
|
599
619
|
const containerName = options.session || generateSessionName('docker');
|
|
620
|
+
const containerExistedBeforeLaunch =
|
|
621
|
+
readDockerContainerStatus(containerName) !== null;
|
|
600
622
|
const cleanupPolicy = getDockerContainerCleanupPolicy(options);
|
|
601
623
|
if (!dockerImageExists(options.image)) {
|
|
602
624
|
// Pass logPath so the image-preparation phase (docker pull) is recorded in
|
|
@@ -671,6 +693,12 @@ function runInDocker(command, options = {}) {
|
|
|
671
693
|
dockerResult.stderr.trim() ||
|
|
672
694
|
dockerResult.stdout.trim() ||
|
|
673
695
|
`docker exited with code ${dockerResult.status}`;
|
|
696
|
+
if (
|
|
697
|
+
!containerExistedBeforeLaunch &&
|
|
698
|
+
readDockerContainerStatus(containerName) === 'created'
|
|
699
|
+
) {
|
|
700
|
+
removeDockerContainer(containerName, options.logPath);
|
|
701
|
+
}
|
|
674
702
|
throw new Error(dockerError);
|
|
675
703
|
}
|
|
676
704
|
|
|
@@ -760,8 +788,17 @@ function runInDocker(command, options = {}) {
|
|
|
760
788
|
}
|
|
761
789
|
|
|
762
790
|
const oomKilled = readDockerContainerOomKilled(containerName);
|
|
791
|
+
const launchFailed =
|
|
792
|
+
!containerExistedBeforeLaunch &&
|
|
793
|
+
readDockerContainerStatus(containerName) === 'created';
|
|
763
794
|
|
|
764
|
-
if (
|
|
795
|
+
if (launchFailed) {
|
|
796
|
+
if (removeDockerContainer(containerName, options.logPath)) {
|
|
797
|
+
message += `\nContainer removed after launch failure.`;
|
|
798
|
+
} else {
|
|
799
|
+
message += `\nWarning: failed to remove container after launch failure.`;
|
|
800
|
+
}
|
|
801
|
+
} else if (
|
|
765
802
|
shouldCleanupDockerContainer(cleanupPolicy, exitCode, oomKilled)
|
|
766
803
|
) {
|
|
767
804
|
if (removeDockerContainer(containerName, options.logPath)) {
|
package/src/lib/usage.js
CHANGED
|
@@ -14,6 +14,8 @@ Options:
|
|
|
14
14
|
--mount <spec> Docker --mount spec (repeatable, docker only)
|
|
15
15
|
--env, -e <KEY=VALUE> Environment variable for docker container (repeatable, docker only)
|
|
16
16
|
--privileged Run docker container in privileged mode (docker only)
|
|
17
|
+
--network <name> Connect docker container to a named network (docker only)
|
|
18
|
+
--network-alias <alias> Add network-scoped alias (repeatable, docker only)
|
|
17
19
|
--endpoint <endpoint> SSH endpoint (required for ssh isolation, e.g., user@host)
|
|
18
20
|
--isolated-user, -u [name] Create isolated user with same permissions
|
|
19
21
|
--keep-user Keep isolated user after command completes
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Real-daemon integration coverage for Docker network isolation (issue #154). */
|
|
3
|
+
|
|
4
|
+
const { after, before, describe, it } = require('node:test');
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const { randomUUID } = require('crypto');
|
|
8
|
+
const { runInDocker } = require('../src/lib/isolation');
|
|
9
|
+
|
|
10
|
+
const suffix = randomUUID().slice(0, 8);
|
|
11
|
+
const network = `start-network-${suffix}`;
|
|
12
|
+
const sidecar = `start-sidecar-${suffix}`;
|
|
13
|
+
const connected = `start-connected-${suffix}`;
|
|
14
|
+
const control = `start-control-${suffix}`;
|
|
15
|
+
const missing = `start-missing-${suffix}`;
|
|
16
|
+
const missingAttached = `start-missing-attached-${suffix}`;
|
|
17
|
+
let dockerAvailable = false;
|
|
18
|
+
|
|
19
|
+
function docker(args) {
|
|
20
|
+
return spawnSync('docker', args, { encoding: 'utf8' });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('Docker named network integration', { timeout: 60000 }, () => {
|
|
24
|
+
before(() => {
|
|
25
|
+
dockerAvailable =
|
|
26
|
+
process.platform === 'linux' && docker(['info']).status === 0;
|
|
27
|
+
if (!dockerAvailable) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const created = docker(['network', 'create', '--internal', network]);
|
|
32
|
+
assert.strictEqual(created.status, 0, created.stderr);
|
|
33
|
+
const started = docker([
|
|
34
|
+
'run',
|
|
35
|
+
'-d',
|
|
36
|
+
'--name',
|
|
37
|
+
sidecar,
|
|
38
|
+
'--network',
|
|
39
|
+
network,
|
|
40
|
+
'--network-alias',
|
|
41
|
+
'formal-ai',
|
|
42
|
+
'alpine:3.23',
|
|
43
|
+
'sleep',
|
|
44
|
+
'60',
|
|
45
|
+
]);
|
|
46
|
+
assert.strictEqual(started.status, 0, started.stderr);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
after(() => {
|
|
50
|
+
if (!dockerAvailable) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
docker(['rm', '-f', sidecar, connected, control, missing, missingAttached]);
|
|
54
|
+
docker(['network', 'rm', network]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('resolves an alias only from containers joined to the internal network', async () => {
|
|
58
|
+
if (!dockerAvailable) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const joined = await runInDocker('ping -c 1 formal-ai', {
|
|
63
|
+
image: 'alpine:3.23',
|
|
64
|
+
session: connected,
|
|
65
|
+
network,
|
|
66
|
+
networkAliases: ['task'],
|
|
67
|
+
detached: true,
|
|
68
|
+
shell: 'sh',
|
|
69
|
+
keepContainer: true,
|
|
70
|
+
});
|
|
71
|
+
assert.strictEqual(joined.success, true, joined.message);
|
|
72
|
+
const joinedExit = docker(['wait', connected]);
|
|
73
|
+
assert.strictEqual(joinedExit.status, 0, joinedExit.stderr);
|
|
74
|
+
assert.strictEqual(joinedExit.stdout.trim(), '0');
|
|
75
|
+
|
|
76
|
+
const unconnected = await runInDocker('ping -c 1 formal-ai', {
|
|
77
|
+
image: 'alpine:3.23',
|
|
78
|
+
session: control,
|
|
79
|
+
shell: 'sh',
|
|
80
|
+
alwaysCleanupContainer: true,
|
|
81
|
+
});
|
|
82
|
+
assert.strictEqual(unconnected.success, false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('fails for a missing network without orphaning a container', async () => {
|
|
86
|
+
if (!dockerAvailable) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = await runInDocker('echo should-not-run', {
|
|
91
|
+
image: 'alpine:3.23',
|
|
92
|
+
session: missing,
|
|
93
|
+
network: `${network}-absent`,
|
|
94
|
+
detached: true,
|
|
95
|
+
shell: 'sh',
|
|
96
|
+
});
|
|
97
|
+
assert.strictEqual(result.success, false);
|
|
98
|
+
assert.match(result.message, /network .* not found/i);
|
|
99
|
+
assert.notStrictEqual(docker(['inspect', missing]).status, 0);
|
|
100
|
+
|
|
101
|
+
const attachedResult = await runInDocker('echo should-not-run', {
|
|
102
|
+
image: 'alpine:3.23',
|
|
103
|
+
session: missingAttached,
|
|
104
|
+
network: `${network}-absent`,
|
|
105
|
+
shell: 'sh',
|
|
106
|
+
});
|
|
107
|
+
assert.strictEqual(attachedResult.success, false);
|
|
108
|
+
assert.notStrictEqual(docker(['inspect', missingAttached]).status, 0);
|
|
109
|
+
|
|
110
|
+
const conflict = await runInDocker('echo should-not-run', {
|
|
111
|
+
image: 'alpine:3.23',
|
|
112
|
+
session: sidecar,
|
|
113
|
+
network: `${network}-absent`,
|
|
114
|
+
detached: true,
|
|
115
|
+
shell: 'sh',
|
|
116
|
+
});
|
|
117
|
+
assert.strictEqual(conflict.success, false);
|
|
118
|
+
assert.strictEqual(docker(['inspect', sidecar]).status, 0);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* Tests for Docker runtime options: --volume/-v, --mount, --env/-e,
|
|
3
|
+
* Tests for Docker runtime options: --volume/-v, --mount, --env/-e,
|
|
4
|
+
* --privileged, --network, and --network-alias
|
|
4
5
|
*
|
|
5
6
|
* Reproduces issue #132: callers need to configure bind mounts, volumes,
|
|
6
7
|
* environment variables, and privileged mode for the docker isolation backend
|
|
@@ -90,12 +91,58 @@ describe('Docker runtime options parsing', () => {
|
|
|
90
91
|
assert.strictEqual(result.wrapperOptions.privileged, true);
|
|
91
92
|
});
|
|
92
93
|
|
|
94
|
+
it('should parse --network value and equals forms without consuming child args', () => {
|
|
95
|
+
const spaced = parseArgs([
|
|
96
|
+
'-i',
|
|
97
|
+
'docker',
|
|
98
|
+
'--network',
|
|
99
|
+
'hive-formal-ai',
|
|
100
|
+
'--',
|
|
101
|
+
'--network',
|
|
102
|
+
'child-network',
|
|
103
|
+
]);
|
|
104
|
+
const equals = parseArgs([
|
|
105
|
+
'-i',
|
|
106
|
+
'docker',
|
|
107
|
+
'--network=hive-formal-ai',
|
|
108
|
+
'--',
|
|
109
|
+
'echo',
|
|
110
|
+
'ok',
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
assert.strictEqual(spaced.wrapperOptions.network, 'hive-formal-ai');
|
|
114
|
+
assert.deepStrictEqual(spaced.rawCommand, ['--network', 'child-network']);
|
|
115
|
+
assert.strictEqual(equals.wrapperOptions.network, 'hive-formal-ai');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should parse repeatable --network-alias value and equals forms', () => {
|
|
119
|
+
const result = parseArgs([
|
|
120
|
+
'-i',
|
|
121
|
+
'docker',
|
|
122
|
+
'--network',
|
|
123
|
+
'hive-formal-ai',
|
|
124
|
+
'--network-alias',
|
|
125
|
+
'formal-ai',
|
|
126
|
+
'--network-alias=checker',
|
|
127
|
+
'--',
|
|
128
|
+
'echo',
|
|
129
|
+
'ok',
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
assert.deepStrictEqual(result.wrapperOptions.networkAliases, [
|
|
133
|
+
'formal-ai',
|
|
134
|
+
'checker',
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
93
138
|
it('should default runtime options to empty/false', () => {
|
|
94
139
|
const result = parseArgs(['-i', 'docker', '--', 'ls']);
|
|
95
140
|
assert.deepStrictEqual(result.wrapperOptions.volumes, []);
|
|
96
141
|
assert.deepStrictEqual(result.wrapperOptions.mounts, []);
|
|
97
142
|
assert.deepStrictEqual(result.wrapperOptions.env, []);
|
|
98
143
|
assert.strictEqual(result.wrapperOptions.privileged, false);
|
|
144
|
+
assert.strictEqual(result.wrapperOptions.network, null);
|
|
145
|
+
assert.deepStrictEqual(result.wrapperOptions.networkAliases, []);
|
|
99
146
|
});
|
|
100
147
|
|
|
101
148
|
it('should throw when --volume requires an argument', () => {
|
|
@@ -136,6 +183,15 @@ describe('Docker runtime options validation', () => {
|
|
|
136
183
|
}, /--privileged option is only valid when isolation stack includes docker/);
|
|
137
184
|
});
|
|
138
185
|
|
|
186
|
+
it('should reject network options without docker', () => {
|
|
187
|
+
assert.throws(() => {
|
|
188
|
+
parseArgs(['-i', 'tmux', '--network', 'private', '--', 'ls']);
|
|
189
|
+
}, /--network option is only valid when isolation stack includes docker/);
|
|
190
|
+
assert.throws(() => {
|
|
191
|
+
parseArgs(['--network-alias', 'service', '--', 'ls']);
|
|
192
|
+
}, /--network-alias option is only valid when isolation stack includes docker/);
|
|
193
|
+
});
|
|
194
|
+
|
|
139
195
|
it('should accept runtime options when stack includes docker', () => {
|
|
140
196
|
const result = parseArgs([
|
|
141
197
|
'-i',
|
|
@@ -171,6 +227,8 @@ describe('buildDockerRuntimeArgs', () => {
|
|
|
171
227
|
env: ['FOO=bar', 'GH_TOKEN=secret'],
|
|
172
228
|
volumes: ['/h/a:/c/a', '/h/b:/c/b:ro'],
|
|
173
229
|
mounts: ['type=bind,src=/h,dst=/c'],
|
|
230
|
+
network: 'hive-formal-ai',
|
|
231
|
+
networkAliases: ['formal-ai', 'checker'],
|
|
174
232
|
});
|
|
175
233
|
assert.deepStrictEqual(args, [
|
|
176
234
|
'--privileged',
|
|
@@ -184,6 +242,12 @@ describe('buildDockerRuntimeArgs', () => {
|
|
|
184
242
|
'/h/b:/c/b:ro',
|
|
185
243
|
'--mount',
|
|
186
244
|
'type=bind,src=/h,dst=/c',
|
|
245
|
+
'--network',
|
|
246
|
+
'hive-formal-ai',
|
|
247
|
+
'--network-alias',
|
|
248
|
+
'formal-ai',
|
|
249
|
+
'--network-alias',
|
|
250
|
+
'checker',
|
|
187
251
|
]);
|
|
188
252
|
});
|
|
189
253
|
});
|
|
@@ -199,12 +263,16 @@ describe('buildDockerRuntimeStatusLines', () => {
|
|
|
199
263
|
mounts: ['type=bind,src=/h,dst=/c'],
|
|
200
264
|
env: ['FOO=bar'],
|
|
201
265
|
privileged: true,
|
|
266
|
+
network: 'hive-formal-ai',
|
|
267
|
+
networkAliases: ['formal-ai', 'checker'],
|
|
202
268
|
});
|
|
203
269
|
assert.deepStrictEqual(lines, [
|
|
204
270
|
'[Isolation] Volumes: /h:/c:ro',
|
|
205
271
|
'[Isolation] Mounts: type=bind,src=/h,dst=/c',
|
|
206
272
|
'[Isolation] Env: FOO=bar',
|
|
207
273
|
'[Isolation] Privileged: true',
|
|
274
|
+
'[Isolation] Network: hive-formal-ai',
|
|
275
|
+
'[Isolation] Network aliases: formal-ai, checker',
|
|
208
276
|
]);
|
|
209
277
|
});
|
|
210
278
|
|
|
@@ -223,6 +291,8 @@ describe('buildDockerRuntimeMetadata', () => {
|
|
|
223
291
|
mounts: null,
|
|
224
292
|
env: null,
|
|
225
293
|
privileged: null,
|
|
294
|
+
network: null,
|
|
295
|
+
networkAliases: null,
|
|
226
296
|
});
|
|
227
297
|
});
|
|
228
298
|
|
|
@@ -233,12 +303,16 @@ describe('buildDockerRuntimeMetadata', () => {
|
|
|
233
303
|
mounts: ['type=bind,src=/h,dst=/c'],
|
|
234
304
|
env: ['FOO=bar'],
|
|
235
305
|
privileged: true,
|
|
306
|
+
network: 'hive-formal-ai',
|
|
307
|
+
networkAliases: ['formal-ai', 'checker'],
|
|
236
308
|
}),
|
|
237
309
|
{
|
|
238
310
|
volumes: ['/h:/c'],
|
|
239
311
|
mounts: ['type=bind,src=/h,dst=/c'],
|
|
240
312
|
env: ['FOO=bar'],
|
|
241
313
|
privileged: true,
|
|
314
|
+
network: 'hive-formal-ai',
|
|
315
|
+
networkAliases: ['formal-ai', 'checker'],
|
|
242
316
|
}
|
|
243
317
|
);
|
|
244
318
|
});
|