start-command 0.37.9 → 0.39.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 +26 -0
- package/README.md +7 -2
- package/bunfig.toml +4 -0
- package/eslint.config.mjs +1 -0
- package/package.json +4 -4
- package/src/bin/cli.js +62 -26
- package/src/lib/args-parser-queries.js +45 -1
- package/src/lib/args-parser.js +19 -0
- package/src/lib/cgroup-memory.js +95 -23
- package/src/lib/command-builder.js +4 -0
- package/src/lib/container-removal.js +4 -5
- package/src/lib/detached-finalize.js +1 -0
- package/src/lib/docker-command-handoff.js +34 -6
- package/src/lib/docker-init-option.js +65 -0
- package/src/lib/docker-inspect-batch.js +176 -0
- package/src/lib/docker-resource-options.js +21 -2
- package/src/lib/docker-runtime-args.js +19 -3
- package/src/lib/execution-attempt.js +19 -9
- package/src/lib/execution-chain.js +182 -0
- package/src/lib/execution-control.js +8 -6
- package/src/lib/execution-env.js +120 -0
- package/src/lib/execution-identity.js +180 -0
- package/src/lib/execution-recovery.js +21 -1
- package/src/lib/execution-resume.js +21 -1
- package/src/lib/execution-store.js +30 -23
- package/src/lib/exit-evidence.js +139 -7
- package/src/lib/isolation.js +21 -3
- package/src/lib/launch-identity-options.js +81 -0
- package/src/lib/launch-identity.js +411 -0
- package/src/lib/log-uploader.js +15 -1
- package/src/lib/output-blocks.js +11 -0
- package/src/lib/query-commands.js +18 -3
- package/src/lib/resume-resources.js +2 -2
- package/src/lib/screen-isolation.js +11 -2
- package/src/lib/spawn-helpers.js +33 -16
- package/src/lib/status-formatter.js +39 -131
- package/src/lib/status-probe.js +149 -0
- package/src/lib/store-decode-cache.js +141 -0
- package/src/lib/usage.js +11 -1
- package/test/args-parser-attach-resume.js +1 -1
- package/test/args-parser-control.js +2 -2
- package/test/cargo-lock-version.mjs +121 -0
- package/test/check-rust-coverage.mjs +71 -0
- package/test/check-web-archive.mjs +154 -0
- package/test/ci-workflow-invariants.js +204 -0
- package/test/command-builder-docker-cleanup.js +24 -0
- package/test/command-exit-codes.mjs +91 -0
- package/test/docker-runtime-options.js +2 -0
- package/test/docker-snapshot-safety.js +39 -0
- package/test/execution-resume.js +2 -0
- package/test/fixtures/fake-npm.sh +64 -0
- package/test/fixtures/lychee-report.md +39 -0
- package/test/javascript-first-ci.js +69 -0
- package/test/links-workflow.mjs +127 -0
- package/test/lint-gate.mjs +56 -0
- package/test/npm-publish.mjs +181 -0
- package/test/recheck-broken-links.mjs +452 -0
- package/test/regression-176.js +2 -0
- package/test/regression-187.js +2 -0
- package/test/regression-191.js +4 -0
- package/test/regression-193.js +2 -0
- package/test/regression-198.js +2 -0
- package/test/regression-207.js +8 -2
- package/test/regression-224.js +2 -0
- package/test/regression-229.js +267 -0
- package/test/regression-230.js +234 -0
- package/test/regression-231.js +298 -0
- package/test/regression-236.js +250 -0
- package/test/regression-238.js +129 -0
- package/test/regression-241.js +254 -0
- package/test/regression-242.js +241 -0
- package/test/regression-243.js +493 -0
- package/test/regression-244.js +471 -0
- package/test/release-push-retry.mjs +172 -0
- package/test/run-js-tests.mjs +15 -3
- package/test/setup/private-snapshot-lock.js +17 -0
- package/test/shared-golden.js +44 -2
- package/test/transient-links.mjs +139 -0
|
@@ -60,6 +60,7 @@ function buildNextLevelCommand(options, command) {
|
|
|
60
60
|
for (const [key, flag] of [
|
|
61
61
|
['memory', '--memory'],
|
|
62
62
|
['memorySwap', '--memory-swap'],
|
|
63
|
+
['memoryReservation', '--memory-reservation'],
|
|
63
64
|
['cpus', '--cpus'],
|
|
64
65
|
['onKillResumeMemory', '--on-kill-resume-memory'],
|
|
65
66
|
]) {
|
|
@@ -112,6 +113,9 @@ function buildNextLevelCommand(options, command) {
|
|
|
112
113
|
if (options.privileged) {
|
|
113
114
|
parts.push('--privileged');
|
|
114
115
|
}
|
|
116
|
+
if (options.init) {
|
|
117
|
+
parts.push('--init');
|
|
118
|
+
}
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
// Pass through global flags
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const { spawnSync } = require('child_process');
|
|
16
|
+
const { prefetchedInspect } = require('./docker-inspect-batch');
|
|
16
17
|
|
|
17
18
|
/** `containerState` of a record whose container no longer exists. */
|
|
18
19
|
const CONTAINER_STATE_REMOVED = 'removed';
|
|
@@ -50,11 +51,9 @@ function isDockerNotFound(result) {
|
|
|
50
51
|
* the inspect failed; `notFound` says the failure was "no such container"
|
|
51
52
|
*/
|
|
52
53
|
function dockerInspect(dockerCommand, spawnOptions, format, target) {
|
|
53
|
-
const result =
|
|
54
|
-
|
|
55
|
-
['inspect', '-f', format, target],
|
|
56
|
-
spawnOptions
|
|
57
|
-
);
|
|
54
|
+
const result =
|
|
55
|
+
prefetchedInspect(format, target) ||
|
|
56
|
+
spawnSync(dockerCommand, ['inspect', '-f', format, target], spawnOptions);
|
|
58
57
|
if (result.error || result.status !== 0 || !result.stdout) {
|
|
59
58
|
return { stdout: null, notFound: isDockerNotFound(result) };
|
|
60
59
|
}
|
|
@@ -12,7 +12,9 @@ function buildCommandHandoffArgs(mainArgs, markerPath) {
|
|
|
12
12
|
return [
|
|
13
13
|
'sh',
|
|
14
14
|
'-c',
|
|
15
|
-
|
|
15
|
+
// `$p.env` refreshes the START_* variables a `docker start` cannot
|
|
16
|
+
// change (issue #230) before either command runs.
|
|
17
|
+
'p=$1; shift; if [ -f "$p.env" ]; then . "$p.env"; fi; if [ -f "$p" ]; then exec sh "$p"; fi; exec "$@"',
|
|
16
18
|
'start-command-handoff',
|
|
17
19
|
markerPath,
|
|
18
20
|
...mainArgs,
|
|
@@ -49,27 +51,53 @@ function buildCommandHandoffScript(command, options = {}) {
|
|
|
49
51
|
return `exec ${shellQuote(shell)}${interactive} -c ${shellQuote(effective)}\n`;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
function
|
|
54
|
+
function copyIntoContainer(containerName, target, content, runner) {
|
|
53
55
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'start-handoff-'));
|
|
54
56
|
const file = path.join(dir, 'command');
|
|
55
57
|
try {
|
|
56
|
-
fs.writeFileSync(file,
|
|
57
|
-
mode: 0o644,
|
|
58
|
-
});
|
|
58
|
+
fs.writeFileSync(file, content, { mode: 0o644 });
|
|
59
59
|
fs.chmodSync(file, 0o644);
|
|
60
60
|
return runner(require('./docker-cleanup').getDockerCommand(), [
|
|
61
61
|
'cp',
|
|
62
62
|
file,
|
|
63
|
-
`${containerName}:${
|
|
63
|
+
`${containerName}:${target}`,
|
|
64
64
|
]);
|
|
65
65
|
} finally {
|
|
66
66
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function writeCommandHandoff(containerName, command, runner, options = {}) {
|
|
71
|
+
return copyIntoContainer(
|
|
72
|
+
containerName,
|
|
73
|
+
commandHandoffPath(containerName),
|
|
74
|
+
buildCommandHandoffScript(command, options),
|
|
75
|
+
runner
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Copy the START_* variables of the next attempt into a stopped container
|
|
81
|
+
* that was created with the handoff selector (issue #230).
|
|
82
|
+
* @param {string} containerName - Container name
|
|
83
|
+
* @param {object} env - Variables from `buildExecutionEnv`
|
|
84
|
+
* @param {Function} runner - Command runner
|
|
85
|
+
* @returns {object} Runner result
|
|
86
|
+
*/
|
|
87
|
+
function writeExecutionEnvHandoff(containerName, env, runner) {
|
|
88
|
+
const statement = require('./execution-env').buildExecutionEnvExport(env);
|
|
89
|
+
return copyIntoContainer(
|
|
90
|
+
containerName,
|
|
91
|
+
`${commandHandoffPath(containerName)}.env`,
|
|
92
|
+
statement ? `${statement}\n` : '',
|
|
93
|
+
runner
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
70
97
|
module.exports = {
|
|
71
98
|
commandHandoffPath,
|
|
72
99
|
buildCommandHandoffArgs,
|
|
73
100
|
buildCommandHandoffScript,
|
|
74
101
|
writeCommandHandoff,
|
|
102
|
+
writeExecutionEnvHandoff,
|
|
75
103
|
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse and validate the opt-in init process for docker isolation
|
|
3
|
+
* (issue #244):
|
|
4
|
+
* --init run `docker run --init`, so Docker's bundled tini is PID 1
|
|
5
|
+
* and reaps orphaned processes
|
|
6
|
+
* --no-init keep the command as PID 1, overriding the environment default
|
|
7
|
+
*
|
|
8
|
+
* Without either flag, START_COMMAND_DOCKER_INIT=1 turns it on for every
|
|
9
|
+
* docker launch on the host. The default stays off: a command that is its own
|
|
10
|
+
* supervisor keeps today's behaviour, and recovery works the same for
|
|
11
|
+
* containers started with or without it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const DOCKER_INIT_ENV = 'START_COMMAND_DOCKER_INIT';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether the environment asks for an init process by default.
|
|
18
|
+
* @param {object} [env] - Environment
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
function dockerInitFromEnv(env = process.env) {
|
|
22
|
+
const value = String(env[DOCKER_INIT_ENV] || '')
|
|
23
|
+
.trim()
|
|
24
|
+
.toLowerCase();
|
|
25
|
+
return ['1', 'true', 'yes', 'on'].includes(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseDockerInitOption(args, index, options) {
|
|
29
|
+
const arg = args[index];
|
|
30
|
+
if (arg === '--init' || arg === '--no-init') {
|
|
31
|
+
options.init = arg === '--init';
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve `options.init` to a boolean: the flag if one was given (`null`
|
|
39
|
+
* means none), otherwise the environment default for launches whose stack
|
|
40
|
+
* includes docker. Idempotent, because `validateOptions` may run again on
|
|
41
|
+
* options it already resolved: `--no-init` (or a resolved `false`) without
|
|
42
|
+
* docker is the default anyway and is accepted.
|
|
43
|
+
* @param {object} options - Parsed options
|
|
44
|
+
* @param {object} [env] - Environment
|
|
45
|
+
* @throws {Error} If `--init` is given without docker isolation
|
|
46
|
+
*/
|
|
47
|
+
function validateDockerInitOption(options, env = process.env) {
|
|
48
|
+
const stack = options.isolatedStack || [options.isolated];
|
|
49
|
+
const hasDocker = options.isolated !== null && stack.includes('docker');
|
|
50
|
+
if (options.init === true && !hasDocker) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'--init option is only valid when isolation stack includes docker'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (typeof options.init !== 'boolean') {
|
|
56
|
+
options.init = hasDocker && dockerInitFromEnv(env);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
DOCKER_INIT_ENV,
|
|
62
|
+
dockerInitFromEnv,
|
|
63
|
+
parse: parseDockerInitOption,
|
|
64
|
+
validate: validateDockerInitOption,
|
|
65
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One `docker inspect` for every container a status query probes (issue #243).
|
|
3
|
+
*
|
|
4
|
+
* `--list` used to start one or two `docker inspect` processes per record
|
|
5
|
+
* (the live state, the process IDs, the removal check), one after another,
|
|
6
|
+
* and `--status` up to three for its one record. While a query runs, this
|
|
7
|
+
* module asks Docker once for every field those probes read, and answers the
|
|
8
|
+
* per-record `docker inspect -f <format> <target>` calls from that answer.
|
|
9
|
+
*
|
|
10
|
+
* Docker prints one line per found container, in argument order, and one
|
|
11
|
+
* `No such container: <target>` line on stderr per missing one. Each line
|
|
12
|
+
* is matched back to its target by name or ID prefix (Docker's own lookup
|
|
13
|
+
* order), so the answer does not depend on the line order. A target the
|
|
14
|
+
* batch cannot account for (an old or stubbed docker, a daemon error) is
|
|
15
|
+
* not answered, and its per-record probe runs as before.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { spawnSync } = require('child_process');
|
|
19
|
+
|
|
20
|
+
/** Fields of the batched inspect, in output order. */
|
|
21
|
+
const BATCH_FIELDS = [
|
|
22
|
+
'.Name',
|
|
23
|
+
'.Id',
|
|
24
|
+
'.State.Pid',
|
|
25
|
+
'.State.Running',
|
|
26
|
+
'.State.ExitCode',
|
|
27
|
+
'.State.OOMKilled',
|
|
28
|
+
'.State.StartedAt',
|
|
29
|
+
'.State.FinishedAt',
|
|
30
|
+
];
|
|
31
|
+
const BATCH_FORMAT = BATCH_FIELDS.map((field) => `{{${field}}}`).join(' ');
|
|
32
|
+
const NOT_FOUND_LINE = /no such (?:object|container):\s*(\S+)/gi;
|
|
33
|
+
const PLACEHOLDER = /\{\{\s*(\.[A-Za-z.]+)\s*\}\}/g;
|
|
34
|
+
|
|
35
|
+
/** Answers of the query in progress: target -> fields, or null (missing). */
|
|
36
|
+
let prefetched = null;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Containers the status probes of these records would inspect. Records
|
|
40
|
+
* whose container is already marked removed are skipped: they are never
|
|
41
|
+
* probed (issue #243).
|
|
42
|
+
* @param {Object[]} records - Execution records
|
|
43
|
+
* @returns {string[]} Unique names and IDs
|
|
44
|
+
*/
|
|
45
|
+
function dockerProbeTargets(records) {
|
|
46
|
+
const targets = new Set();
|
|
47
|
+
for (const record of records) {
|
|
48
|
+
const opts = (record && record.options) || {};
|
|
49
|
+
if (opts.isolated !== 'docker' || record.containerState === 'removed') {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
for (const target of [opts.sessionName, opts.containerId]) {
|
|
53
|
+
if (typeof target === 'string' && target) {
|
|
54
|
+
targets.add(target);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return [...targets];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Match a batched inspect answer to its targets.
|
|
63
|
+
* @param {string[]} targets - Names and IDs passed to docker
|
|
64
|
+
* @param {string} stdout - One line of `BATCH_FORMAT` per found container
|
|
65
|
+
* @param {string} stderr - Docker's errors
|
|
66
|
+
* @returns {Map<string, Object|null>} Fields per answered target, null for
|
|
67
|
+
* a target Docker reported missing; unanswered targets are absent
|
|
68
|
+
*/
|
|
69
|
+
function parseBatchInspect(targets, stdout, stderr) {
|
|
70
|
+
const rows = String(stdout || '')
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map((line) => line.trim().split(/\s+/))
|
|
73
|
+
.filter((parts) => parts.length === BATCH_FIELDS.length)
|
|
74
|
+
.map((parts) =>
|
|
75
|
+
Object.fromEntries(BATCH_FIELDS.map((field, i) => [field, parts[i]]))
|
|
76
|
+
);
|
|
77
|
+
const missing = new Set(
|
|
78
|
+
[...String(stderr || '').matchAll(NOT_FOUND_LINE)].map((m) => m[1])
|
|
79
|
+
);
|
|
80
|
+
const answers = new Map();
|
|
81
|
+
for (const target of targets) {
|
|
82
|
+
// Docker's own "no such container" wins over a prefix of another
|
|
83
|
+
// target's ID.
|
|
84
|
+
const row = missing.has(target)
|
|
85
|
+
? null
|
|
86
|
+
: rows.find((r) => r['.Name'] === `/${target}`) ||
|
|
87
|
+
rows.find((r) => r['.Id'].startsWith(target));
|
|
88
|
+
if (row !== undefined) {
|
|
89
|
+
answers.set(target, row);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return answers;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Run the batched inspect.
|
|
97
|
+
* @returns {Map<string, Object|null>} See `parseBatchInspect`
|
|
98
|
+
*/
|
|
99
|
+
function runBatchInspect(targets, dockerCommand, spawnOptions) {
|
|
100
|
+
if (targets.length === 0) {
|
|
101
|
+
return new Map();
|
|
102
|
+
}
|
|
103
|
+
const result = spawnSync(
|
|
104
|
+
dockerCommand,
|
|
105
|
+
['inspect', '--type', 'container', '-f', BATCH_FORMAT, ...targets],
|
|
106
|
+
spawnOptions
|
|
107
|
+
);
|
|
108
|
+
if (result.error) {
|
|
109
|
+
return new Map();
|
|
110
|
+
}
|
|
111
|
+
return parseBatchInspect(targets, result.stdout, result.stderr);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run `fn` with the docker probes of `records` answered by one inspect.
|
|
116
|
+
* Nested calls reuse the outer answer.
|
|
117
|
+
* @param {Object[]} records - Records the query will probe
|
|
118
|
+
* @param {{dockerCommand: string, spawnOptions: Object}} docker - How to run docker
|
|
119
|
+
* @param {function(): *} fn - The query
|
|
120
|
+
* @returns {*} What `fn` returns
|
|
121
|
+
*/
|
|
122
|
+
function withDockerInspectBatch(records, docker, fn) {
|
|
123
|
+
if (prefetched) {
|
|
124
|
+
return fn();
|
|
125
|
+
}
|
|
126
|
+
prefetched = runBatchInspect(
|
|
127
|
+
dockerProbeTargets(records),
|
|
128
|
+
docker.dockerCommand,
|
|
129
|
+
docker.spawnOptions
|
|
130
|
+
);
|
|
131
|
+
try {
|
|
132
|
+
return fn();
|
|
133
|
+
} finally {
|
|
134
|
+
prefetched = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Answer `docker inspect -f <format> <target>` from the batch.
|
|
140
|
+
* @param {string} format - Go template of the per-record probe
|
|
141
|
+
* @param {string} target - Container name or ID
|
|
142
|
+
* @returns {{success: boolean, status: number, stdout: string,
|
|
143
|
+
* stderr: string}|null} A spawn-like result, or null when the batch has
|
|
144
|
+
* no answer and the probe must run
|
|
145
|
+
*/
|
|
146
|
+
function prefetchedInspect(format, target) {
|
|
147
|
+
if (!prefetched || !prefetched.has(target)) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const row = prefetched.get(target);
|
|
151
|
+
if (row === null) {
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
status: 1,
|
|
155
|
+
stdout: '',
|
|
156
|
+
stderr: `Error response from daemon: No such container: ${target}\n`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
let known = true;
|
|
160
|
+
const stdout = format.replace(PLACEHOLDER, (_, field) => {
|
|
161
|
+
known = known && field in row;
|
|
162
|
+
return row[field];
|
|
163
|
+
});
|
|
164
|
+
if (!known || stdout.includes('{{')) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return { success: true, status: 0, stdout: `${stdout}\n`, stderr: '' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
BATCH_FORMAT,
|
|
172
|
+
dockerProbeTargets,
|
|
173
|
+
parseBatchInspect,
|
|
174
|
+
prefetchedInspect,
|
|
175
|
+
withDockerInspectBatch,
|
|
176
|
+
};
|
|
@@ -4,6 +4,7 @@ const { normalizeResourceLimits } = require('./docker-resource-limits');
|
|
|
4
4
|
const FIELDS = {
|
|
5
5
|
'--memory': 'memory',
|
|
6
6
|
'--memory-swap': 'memorySwap',
|
|
7
|
+
'--memory-reservation': 'memoryReservation',
|
|
7
8
|
'--cpus': 'cpus',
|
|
8
9
|
'--on-kill-resume-memory': 'onKillResumeMemory',
|
|
9
10
|
};
|
|
@@ -95,7 +96,7 @@ function readDaemonCapacity(runner) {
|
|
|
95
96
|
|
|
96
97
|
function resolveResourceOptions(options, runner, random = Math.random) {
|
|
97
98
|
const specs = {};
|
|
98
|
-
for (const field of ['memory', 'memorySwap', 'cpus']) {
|
|
99
|
+
for (const field of ['memory', 'memorySwap', 'memoryReservation', 'cpus']) {
|
|
99
100
|
if (options[field] !== null && options[field] !== undefined) {
|
|
100
101
|
specs[field] = parseResourceSpec(options[field], field === 'cpus');
|
|
101
102
|
}
|
|
@@ -137,8 +138,26 @@ function resolveResourceOptions(options, runner, random = Math.random) {
|
|
|
137
138
|
) {
|
|
138
139
|
throw new Error('memory-swap must be at least memory');
|
|
139
140
|
}
|
|
141
|
+
// Compare effective values so lowering a cap also checks a saved reservation.
|
|
142
|
+
const existingReservation = limits
|
|
143
|
+
.find((flag) => flag.startsWith('--memory-reservation='))
|
|
144
|
+
?.slice('--memory-reservation='.length);
|
|
145
|
+
const reservation =
|
|
146
|
+
resolved.memoryReservation ??
|
|
147
|
+
(existingReservation
|
|
148
|
+
? parseResourceSpec(existingReservation).amount
|
|
149
|
+
: null);
|
|
150
|
+
if (
|
|
151
|
+
memory !== null &&
|
|
152
|
+
memory !== undefined &&
|
|
153
|
+
reservation !== null &&
|
|
154
|
+
reservation !== undefined &&
|
|
155
|
+
reservation >= memory
|
|
156
|
+
) {
|
|
157
|
+
throw new Error('memory-reservation must be less than memory');
|
|
158
|
+
}
|
|
140
159
|
for (const [field, amount] of Object.entries(resolved)) {
|
|
141
|
-
const flag =
|
|
160
|
+
const flag = Object.keys(FIELDS).find((key) => FIELDS[key] === field);
|
|
142
161
|
for (let i = limits.length - 1; i >= 0; i--) {
|
|
143
162
|
if (
|
|
144
163
|
limits[i].startsWith(`${flag}=`) ||
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Docker runtime options shared by `docker run`, `[Isolation]` status lines
|
|
3
|
-
* and execution-record metadata: privileged mode,
|
|
4
|
-
*
|
|
3
|
+
* and execution-record metadata: privileged mode, the init process
|
|
4
|
+
* (issue #244), environment, volumes, mounts, networks (issue #150) and
|
|
5
|
+
* resource limits (issue #176).
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
const { getDockerNetworks } = require('./docker-network-lifecycle');
|
|
@@ -31,7 +32,7 @@ function getOnKillResume(options = {}) {
|
|
|
31
32
|
* mounts, --mount specs, network configuration and resource limits. Returned
|
|
32
33
|
* in a stable order so they can be spliced into the `docker run` argv before
|
|
33
34
|
* the image name.
|
|
34
|
-
* @param {object} options - Options (privileged, env, volumes, mounts, network, networkAliases, resourceLimits)
|
|
35
|
+
* @param {object} options - Options (privileged, init, env, volumes, mounts, network, networkAliases, resourceLimits)
|
|
35
36
|
* @returns {string[]} Docker CLI arguments
|
|
36
37
|
*/
|
|
37
38
|
function buildDockerRuntimeArgs(options = {}) {
|
|
@@ -39,9 +40,20 @@ function buildDockerRuntimeArgs(options = {}) {
|
|
|
39
40
|
if (options.privileged) {
|
|
40
41
|
args.push('--privileged');
|
|
41
42
|
}
|
|
43
|
+
// tini as PID 1 reaps orphaned processes (issue #244).
|
|
44
|
+
if (options.init) {
|
|
45
|
+
args.push('--init');
|
|
46
|
+
}
|
|
42
47
|
for (const envVar of options.env || []) {
|
|
43
48
|
args.push('-e', envVar);
|
|
44
49
|
}
|
|
50
|
+
// Execution identity (issue #230); last, so a user `--env` cannot spoof it.
|
|
51
|
+
const executionEnv = require('./execution-env');
|
|
52
|
+
for (const envVar of executionEnv.executionEnvAssignments(
|
|
53
|
+
executionEnv.executionEnvFromOptions(options)
|
|
54
|
+
)) {
|
|
55
|
+
args.push('-e', envVar);
|
|
56
|
+
}
|
|
45
57
|
for (const label of require('./docker-labels').dockerLabels(options)) {
|
|
46
58
|
args.push('--label', label);
|
|
47
59
|
}
|
|
@@ -87,6 +99,9 @@ function buildDockerRuntimeStatusLines(options = {}) {
|
|
|
87
99
|
if (options.privileged) {
|
|
88
100
|
lines.push(`[Isolation] Privileged: true`);
|
|
89
101
|
}
|
|
102
|
+
if (options.init) {
|
|
103
|
+
lines.push(`[Isolation] Init: true`);
|
|
104
|
+
}
|
|
90
105
|
const networks = getDockerNetworks(options);
|
|
91
106
|
if (networks.length > 0) {
|
|
92
107
|
lines.push(`[Isolation] Network: ${networks[0]}`);
|
|
@@ -138,6 +153,7 @@ function buildDockerRuntimeMetadata(options = {}) {
|
|
|
138
153
|
labels: options.labels && options.labels.length > 0 ? options.labels : null,
|
|
139
154
|
commandHandoff: options.detached ? true : null,
|
|
140
155
|
privileged: options.privileged || null,
|
|
156
|
+
init: options.init || null,
|
|
141
157
|
network: networks[0] || null,
|
|
142
158
|
networks: networks.length > 0 ? networks : null,
|
|
143
159
|
networkAliases:
|
|
@@ -46,6 +46,21 @@ function readAttemptActivity(record) {
|
|
|
46
46
|
return record;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Number of the record's current attempt: 1 until it is resumed. */
|
|
50
|
+
function currentAttemptNumber(record) {
|
|
51
|
+
return (
|
|
52
|
+
record?.attempt?.number ||
|
|
53
|
+
(Number(record?.options?.resumeCount) || 0) +
|
|
54
|
+
(Number(record?.options?.recoveryAttempts) || 0) +
|
|
55
|
+
1
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Number the next attempt of `record` gets (`START_ATTEMPT`, issue #230). */
|
|
60
|
+
function nextAttemptNumber(record) {
|
|
61
|
+
return currentAttemptNumber(record) + 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
function createAttempt(record, plan) {
|
|
50
65
|
let logOffset = null;
|
|
51
66
|
if (record.logPath) {
|
|
@@ -58,11 +73,7 @@ function createAttempt(record, plan) {
|
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
return {
|
|
61
|
-
number:
|
|
62
|
-
(record.attempt?.number ||
|
|
63
|
-
(Number(record.options?.resumeCount) || 0) +
|
|
64
|
-
(Number(record.options?.recoveryAttempts) || 0) +
|
|
65
|
-
1) + 1,
|
|
76
|
+
number: nextAttemptNumber(record),
|
|
66
77
|
startedAt: new Date().toISOString(),
|
|
67
78
|
logOffset,
|
|
68
79
|
lastOutputAt: null,
|
|
@@ -78,10 +89,7 @@ function archiveAttempt(record) {
|
|
|
78
89
|
readAttemptActivity(record);
|
|
79
90
|
const snapshot = {
|
|
80
91
|
...(record.attempt || {
|
|
81
|
-
number:
|
|
82
|
-
(Number(record.options?.resumeCount) || 0) +
|
|
83
|
-
(Number(record.options?.recoveryAttempts) || 0) +
|
|
84
|
-
1,
|
|
92
|
+
number: currentAttemptNumber(record),
|
|
85
93
|
startedAt: record.options?.resumedAt || record.startTime,
|
|
86
94
|
logOffset: null,
|
|
87
95
|
}),
|
|
@@ -176,6 +184,8 @@ module.exports = {
|
|
|
176
184
|
appendLifecycle,
|
|
177
185
|
archiveAttempt,
|
|
178
186
|
createAttempt,
|
|
187
|
+
currentAttemptNumber,
|
|
188
|
+
nextAttemptNumber,
|
|
179
189
|
patchAttempt,
|
|
180
190
|
readAttemptActivity,
|
|
181
191
|
readAttemptLogTail,
|