start-command 0.37.7 → 0.37.9
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 +16 -0
- package/README.md +5 -0
- package/package.json +1 -1
- package/src/bin/cli.js +15 -0
- package/src/lib/args-parser-queries.js +45 -8
- package/src/lib/args-parser.js +10 -1
- package/src/lib/container-removal.js +163 -0
- package/src/lib/detached-finalize.js +29 -2
- package/src/lib/docker-cleanup.js +7 -4
- package/src/lib/docker-post-mortem.js +2 -0
- package/src/lib/docker-snapshot-resume.js +222 -0
- package/src/lib/docker-snapshot-safety.js +124 -12
- package/src/lib/execution-attempt.js +2 -0
- package/src/lib/execution-cleanup.js +193 -0
- package/src/lib/execution-control.js +2 -1
- package/src/lib/execution-recovery.js +2 -0
- package/src/lib/execution-resume.js +60 -6
- package/src/lib/execution-store.js +52 -0
- package/src/lib/isolation.js +4 -0
- package/src/lib/query-commands.js +16 -3
- package/src/lib/snapshot-resume-options.js +174 -0
- package/src/lib/status-formatter.js +59 -34
- package/src/lib/status-links-notation.js +97 -0
- package/src/lib/store-lock.js +3 -0
- package/src/lib/usage.js +7 -0
- package/test/regression-222.js +61 -0
- package/test/regression-224.js +807 -0
- package/test/regression-225.js +415 -0
- package/test/regression-226.js +81 -0
- package/test/shared-golden.js +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.37.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 55ed66d: A Docker snapshot resume can now be supervised safely. `--snapshot-wait <seconds>` (or `START_SNAPSHOT_LOCK_WAIT`) waits for another resume's snapshot on the host and says once which container it is waiting for. `--snapshot-disk-wait <seconds>` (or `START_SNAPSHOT_DISK_WAIT`) re-checks free disk instead of refusing at once. A busy lock and disk that is still short now exit 75 (`EX_TEMPFAIL`), so a caller can tell "retry later" from a real failure. `--remove-original-after <seconds>` removes the original only if the replacement is still running after the delay. `--snapshot-size-hint <bytes>` skips the `docker inspect --size` walk, and `--snapshot-prepare <command>` runs a cleanup command in the started original before the commit. A long `docker commit` writes its elapsed time to the log every minute. The `start-command.snapshot-image` label is documented as a stable interface, and `$ --cleanup <uuid-or-session-name>` removes an execution's containers and then their snapshot images, never forcing a removal (#224).
|
|
8
|
+
|
|
9
|
+
`$ --status` now reports `containerState removed` and `containerRemovedObservedAt` when the docker container of a finished detached execution no longer exists, instead of showing the old `containerId` as if the container still held its disk. The removal is recorded when start's own cleanup policy removes the container and when `--status` finds it gone; `containerId` is kept for history, and a docker daemon that cannot be reached is not mistaken for a removed container (#225).
|
|
10
|
+
|
|
11
|
+
`$ --status` no longer prints `[object Object]` for nested option values such as `launchOwner`, `exitEvidence`, `resolvedLimits` and `resourceLimitSpecs`: the links-notation output nests them like other objects and the text output prints them as compact JSON (#226).
|
|
12
|
+
|
|
13
|
+
## 0.37.8
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 9ffcae7: A closed stdout/stderr pipe (`$ -- cmd | head -1`) no longer crashes start with an unhandled `EPIPE` and exit code 1: the command keeps running, its log and record are written, and start exits with the command's own exit code (#222).
|
|
18
|
+
|
|
3
19
|
## 0.37.7
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -40,6 +40,11 @@ checked examples.
|
|
|
40
40
|
containers support copy-free command handoff; legacy snapshots require capacity
|
|
41
41
|
preflight. Log uploads are private and sanitized; manual `--no-sanitize` opts out.
|
|
42
42
|
|
|
43
|
+
Snapshot resumes take `--snapshot-wait`, `--snapshot-disk-wait`,
|
|
44
|
+
`--remove-original-after`, `--snapshot-size-hint` and `--snapshot-prepare`, and
|
|
45
|
+
exit 75 when they should be retried later; `--cleanup <id>` removes one
|
|
46
|
+
execution's containers and snapshot images without forcing.
|
|
47
|
+
|
|
43
48
|
## Development
|
|
44
49
|
|
|
45
50
|
```bash
|
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -42,6 +42,17 @@ const { resolveChildExitCode } = require('../lib/exit-reason');
|
|
|
42
42
|
const { printUsage } = require('../lib/usage');
|
|
43
43
|
const { buildDisplayCommand, getCommandName } = require('../lib/shell-utils');
|
|
44
44
|
|
|
45
|
+
// A closed stdout/stderr pipe (`$ --version | head -1`) must neither crash
|
|
46
|
+
// start nor replace the command's exit code: drop the failed output and keep
|
|
47
|
+
// running, logging and recording as usual (issue #222).
|
|
48
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
49
|
+
stream.on('error', (err) => {
|
|
50
|
+
if (err.code !== 'EPIPE' && err.code !== 'ERR_STREAM_DESTROYED') {
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
// Configuration from environment variables
|
|
46
57
|
const config = {
|
|
47
58
|
// Disable automatic issue creation (useful for testing)
|
|
@@ -538,6 +549,10 @@ async function runWithIsolation(
|
|
|
538
549
|
if (mode !== 'detached') {
|
|
539
550
|
executionRecord.complete(exitCode);
|
|
540
551
|
}
|
|
552
|
+
if (result.containerRemoved) {
|
|
553
|
+
// The cleanup policy removed the container (issue #225).
|
|
554
|
+
require('../lib/container-removal').markContainerRemoved(executionRecord);
|
|
555
|
+
}
|
|
541
556
|
try {
|
|
542
557
|
store.save(
|
|
543
558
|
mode === 'detached'
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Query modes: --status, --list [--running]
|
|
9
9
|
* Control modes: --upload-log, --stop, --terminate, --attach [--read-only],
|
|
10
|
-
* --resume [-- <command>], --resume-all,
|
|
10
|
+
* --resume [-- <command>], --resume-all,
|
|
11
|
+
* --cleanup [<uuid-or-session-name>]
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -37,6 +38,7 @@ function createQueryOptionDefaults() {
|
|
|
37
38
|
resumeAll: false, // Re-attach/resume every execution still marked running
|
|
38
39
|
cleanup: false, // Clean up stale "executing" records
|
|
39
40
|
cleanupDryRun: false, // Show what would be cleaned without cleaning
|
|
41
|
+
cleanupTarget: null, // UUID/session name whose containers and images --cleanup removes
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -75,15 +77,45 @@ const FLAG_OPTIONS = {
|
|
|
75
77
|
'--resume-all': (options) => {
|
|
76
78
|
options.resumeAll = true;
|
|
77
79
|
},
|
|
78
|
-
'--cleanup': (options) => {
|
|
79
|
-
options.cleanup = true;
|
|
80
|
-
},
|
|
81
|
-
'--cleanup-dry-run': (options) => {
|
|
82
|
-
options.cleanup = true;
|
|
83
|
-
options.cleanupDryRun = true;
|
|
84
|
-
},
|
|
85
80
|
};
|
|
86
81
|
|
|
82
|
+
/**
|
|
83
|
+
* `--cleanup` and `--cleanup-dry-run` take an optional execution: without one
|
|
84
|
+
* they clean up stale records, with one they remove that execution's Docker
|
|
85
|
+
* containers and snapshot images (issue #224).
|
|
86
|
+
*/
|
|
87
|
+
const CLEANUP_OPTIONS = {
|
|
88
|
+
'--cleanup': false,
|
|
89
|
+
'--cleanup-dry-run': true,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function parseCleanupOption(args, index, options) {
|
|
93
|
+
const arg = args[index];
|
|
94
|
+
const equals = arg.indexOf('=');
|
|
95
|
+
const name = equals === -1 ? arg : arg.slice(0, equals);
|
|
96
|
+
if (!(name in CLEANUP_OPTIONS)) {
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
options.cleanup = true;
|
|
100
|
+
options.cleanupDryRun = options.cleanupDryRun || CLEANUP_OPTIONS[name];
|
|
101
|
+
if (equals !== -1) {
|
|
102
|
+
const value = arg.slice(equals + 1);
|
|
103
|
+
if (!value) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Option ${name} requires a UUID or session name argument`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
options.cleanupTarget = value;
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
const next = args[index + 1];
|
|
112
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
113
|
+
options.cleanupTarget = next;
|
|
114
|
+
return 2;
|
|
115
|
+
}
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
87
119
|
function hasValue(value) {
|
|
88
120
|
return value !== null && value !== undefined;
|
|
89
121
|
}
|
|
@@ -98,6 +130,11 @@ function hasValue(value) {
|
|
|
98
130
|
function parseQueryOption(args, index, options) {
|
|
99
131
|
const arg = args[index];
|
|
100
132
|
|
|
133
|
+
const cleanupConsumed = parseCleanupOption(args, index, options);
|
|
134
|
+
if (cleanupConsumed > 0) {
|
|
135
|
+
return cleanupConsumed;
|
|
136
|
+
}
|
|
137
|
+
|
|
101
138
|
const flag = FLAG_OPTIONS[arg];
|
|
102
139
|
if (flag) {
|
|
103
140
|
flag(options);
|
package/src/lib/args-parser.js
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* --read-only Follow --attach output without forwarding stdin
|
|
38
38
|
* --resume <uuid-or-session> [-- cmd] Resume a stored execution (optionally with a new command)
|
|
39
39
|
* --resume-all Re-attach or reconcile every execution still marked running
|
|
40
|
-
* --cleanup
|
|
40
|
+
* --cleanup [<uuid-or-session>] Clean up stale "executing" records, or one execution's containers and snapshot images
|
|
41
41
|
* --cleanup-dry-run Show stale records that would be cleaned up (without actually cleaning)
|
|
42
42
|
*/
|
|
43
43
|
|
|
@@ -46,6 +46,7 @@ const dockerNetworkOptions = require('./docker-network-options');
|
|
|
46
46
|
const dockerRecoveryOptions = require('./docker-recovery-options');
|
|
47
47
|
const cpuPenalty = require('./cpu-penalty');
|
|
48
48
|
const dockerResourceOptions = require('./docker-resource-options');
|
|
49
|
+
const snapshotResumeOptions = require('./snapshot-resume-options');
|
|
49
50
|
const { parseSequence, isSequence } = require('./sequence-parser');
|
|
50
51
|
const { buildCommandString } = require('./shell-utils');
|
|
51
52
|
const {
|
|
@@ -215,6 +216,7 @@ function parseArgs(args) {
|
|
|
215
216
|
shell: 'auto', // Shell to use in isolation environments: auto, bash, zsh, sh
|
|
216
217
|
useCommandStream: false, // Use command-stream library for command execution
|
|
217
218
|
...createQueryOptionDefaults(),
|
|
219
|
+
...snapshotResumeOptions.createSnapshotOptionDefaults(),
|
|
218
220
|
};
|
|
219
221
|
|
|
220
222
|
let commandArgs = [];
|
|
@@ -566,6 +568,12 @@ function parseOption(args, index, options) {
|
|
|
566
568
|
return 1;
|
|
567
569
|
}
|
|
568
570
|
|
|
571
|
+
// Snapshot resume options (--snapshot-wait, --remove-original-after, ...)
|
|
572
|
+
const snapshotConsumed = snapshotResumeOptions.parse(args, index, options);
|
|
573
|
+
if (snapshotConsumed > 0) {
|
|
574
|
+
return snapshotConsumed;
|
|
575
|
+
}
|
|
576
|
+
|
|
569
577
|
// Query/control options (--status, --list, --attach, --resume, ...)
|
|
570
578
|
const queryConsumed = parseQueryOption(args, index, options);
|
|
571
579
|
if (queryConsumed > 0) {
|
|
@@ -820,6 +828,7 @@ function validateOptions(options) {
|
|
|
820
828
|
throw new Error('--keep-user option is only valid with --isolated-user');
|
|
821
829
|
}
|
|
822
830
|
|
|
831
|
+
snapshotResumeOptions.validate(options);
|
|
823
832
|
validateQueryOptions(options);
|
|
824
833
|
|
|
825
834
|
// Validate shell option
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removed-container bookkeeping for `--status` (issue #225).
|
|
3
|
+
*
|
|
4
|
+
* A kept docker container can be removed later (by hand, by an orchestrator,
|
|
5
|
+
* or by start-command's own cleanup policy). `docker inspect` then exits 1
|
|
6
|
+
* with `No such object` / `No such container`, which used to be folded into
|
|
7
|
+
* the same `null` as "docker could not be queried at all", so `--status` kept
|
|
8
|
+
* reporting the old `containerId` as if the container still held its disk.
|
|
9
|
+
*
|
|
10
|
+
* This module tells the two failures apart and records the removal on the
|
|
11
|
+
* execution record as `containerState: removed` plus
|
|
12
|
+
* `containerRemovedObservedAt`. `containerId` is kept for history.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { spawnSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
/** `containerState` of a record whose container no longer exists. */
|
|
18
|
+
const CONTAINER_STATE_REMOVED = 'removed';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Docker's answer for a container that does not exist:
|
|
22
|
+
* `Error: No such object: <name>` (`docker inspect`, Docker 29 prints it
|
|
23
|
+
* lower-case) and `Error response from daemon: No such container: <name>`
|
|
24
|
+
* (`docker container inspect`). A daemon that cannot be reached prints
|
|
25
|
+
* "Cannot connect" / "failed to connect", which must not match.
|
|
26
|
+
*/
|
|
27
|
+
const DOCKER_NOT_FOUND_PATTERN = /no such (object|container)/i;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Environment variable the detached watcher sets for the finalizer when its
|
|
31
|
+
* own `docker rm -f` removed the container.
|
|
32
|
+
*/
|
|
33
|
+
const CONTAINER_REMOVED_ENV = 'START_INTERNAL_CONTAINER_REMOVED';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {{status?: number|null, stderr?: string|Buffer, error?: Error}} result
|
|
37
|
+
* `spawnSync` result of a `docker inspect`
|
|
38
|
+
* @returns {boolean} True only when docker answered that the object is unknown
|
|
39
|
+
*/
|
|
40
|
+
function isDockerNotFound(result) {
|
|
41
|
+
if (!result || result.error || result.status === 0) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return DOCKER_NOT_FOUND_PATTERN.test(String(result.stderr || ''));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run `docker inspect -f <format> <target>`.
|
|
49
|
+
* @returns {{stdout: string|null, notFound: boolean}} `stdout` is null when
|
|
50
|
+
* the inspect failed; `notFound` says the failure was "no such container"
|
|
51
|
+
*/
|
|
52
|
+
function dockerInspect(dockerCommand, spawnOptions, format, target) {
|
|
53
|
+
const result = spawnSync(
|
|
54
|
+
dockerCommand,
|
|
55
|
+
['inspect', '-f', format, target],
|
|
56
|
+
spawnOptions
|
|
57
|
+
);
|
|
58
|
+
if (result.error || result.status !== 0 || !result.stdout) {
|
|
59
|
+
return { stdout: null, notFound: isDockerNotFound(result) };
|
|
60
|
+
}
|
|
61
|
+
return { stdout: String(result.stdout), notFound: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Set `containerState: removed` on a record, keeping an earlier observation
|
|
66
|
+
* time when there is one.
|
|
67
|
+
* @param {Object} record - Execution record (mutated)
|
|
68
|
+
* @param {string} [observedAt] - When the removal was observed
|
|
69
|
+
* @returns {Object} The same record
|
|
70
|
+
*/
|
|
71
|
+
function markContainerRemoved(record, observedAt = new Date().toISOString()) {
|
|
72
|
+
record.containerState = CONTAINER_STATE_REMOVED;
|
|
73
|
+
record.containerRemovedObservedAt =
|
|
74
|
+
record.containerRemovedObservedAt || observedAt;
|
|
75
|
+
return record;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether a finished detached docker record should be checked for removal.
|
|
80
|
+
* Executing records are never touched: a container that is not visible yet
|
|
81
|
+
* on a slow Docker-in-Docker host is not a removed one (issue #136).
|
|
82
|
+
*/
|
|
83
|
+
function shouldProbeRemoval(record) {
|
|
84
|
+
const opts = (record && record.options) || {};
|
|
85
|
+
return (
|
|
86
|
+
record.status === 'executed' &&
|
|
87
|
+
opts.isolated === 'docker' &&
|
|
88
|
+
opts.isolationMode === 'detached' &&
|
|
89
|
+
Boolean(opts.sessionName || opts.containerId) &&
|
|
90
|
+
record.containerState !== CONTAINER_STATE_REMOVED
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Attach `containerState: removed` to a finished detached docker record whose
|
|
96
|
+
* container no longer exists. The container id is preferred over the session
|
|
97
|
+
* name, so a later container that reuses the name is not mistaken for it.
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} record - Enriched execution record
|
|
100
|
+
* @param {{dockerCommand: string, spawnOptions: Object,
|
|
101
|
+
* sessionNotFound?: boolean}} probe - How to run docker, and whether the
|
|
102
|
+
* status probe by session name already answered "no such container"
|
|
103
|
+
* @returns {Object} The record, or a copy carrying the removal
|
|
104
|
+
*/
|
|
105
|
+
function attachContainerRemoval(record, probe) {
|
|
106
|
+
if (!shouldProbeRemoval(record)) {
|
|
107
|
+
return record;
|
|
108
|
+
}
|
|
109
|
+
const containerId = record.options.containerId;
|
|
110
|
+
const notFound = containerId
|
|
111
|
+
? dockerInspect(
|
|
112
|
+
probe.dockerCommand,
|
|
113
|
+
probe.spawnOptions,
|
|
114
|
+
'{{.Id}}',
|
|
115
|
+
containerId
|
|
116
|
+
).notFound
|
|
117
|
+
: probe.sessionNotFound === true;
|
|
118
|
+
if (!notFound) {
|
|
119
|
+
return record;
|
|
120
|
+
}
|
|
121
|
+
const copy = Object.assign(
|
|
122
|
+
Object.create(Object.getPrototypeOf(record)),
|
|
123
|
+
record
|
|
124
|
+
);
|
|
125
|
+
return markContainerRemoved(copy);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Persist a removal `--status` observed, best effort: a read-only query must
|
|
130
|
+
* never fail because the store could not be written.
|
|
131
|
+
* @param {Object} store - ExecutionStore
|
|
132
|
+
* @param {Object} stored - Record as read from the store
|
|
133
|
+
* @param {Object} enriched - Record as reported
|
|
134
|
+
*/
|
|
135
|
+
function persistContainerRemoval(store, stored, enriched) {
|
|
136
|
+
if (
|
|
137
|
+
!store ||
|
|
138
|
+
enriched.containerState !== CONTAINER_STATE_REMOVED ||
|
|
139
|
+
stored.containerState === CONTAINER_STATE_REMOVED ||
|
|
140
|
+
typeof store.markContainerRemoved !== 'function'
|
|
141
|
+
) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
store.markContainerRemoved(
|
|
146
|
+
stored.uuid,
|
|
147
|
+
enriched.containerRemovedObservedAt
|
|
148
|
+
);
|
|
149
|
+
} catch {
|
|
150
|
+
// The removal is still reported; it is simply observed again next time.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
CONTAINER_REMOVED_ENV,
|
|
156
|
+
CONTAINER_STATE_REMOVED,
|
|
157
|
+
DOCKER_NOT_FOUND_PATTERN,
|
|
158
|
+
attachContainerRemoval,
|
|
159
|
+
dockerInspect,
|
|
160
|
+
isDockerNotFound,
|
|
161
|
+
markContainerRemoved,
|
|
162
|
+
persistContainerRemoval,
|
|
163
|
+
};
|
|
@@ -63,6 +63,8 @@ const WATCHER_LOST_CONTAINER = 'watcher-lost-container';
|
|
|
63
63
|
* @param {boolean|string|null} [options.running] - `.State.Running`
|
|
64
64
|
* @param {string|null} [options.cgroupMemory] - Last cgroup v2 sample of the
|
|
65
65
|
* watcher, `<memory.max> <memory.peak> <oom> <oom_kill>` (issue #182)
|
|
66
|
+
* @param {boolean|string|null} [options.containerRemoved] - The watcher's own
|
|
67
|
+
* `docker rm -f` removed the container (issue #225)
|
|
66
68
|
* @returns {{updated: boolean, reason?: string, record?: Object}} Outcome
|
|
67
69
|
*/
|
|
68
70
|
function finalizeDetachedExecution(options = {}) {
|
|
@@ -96,9 +98,23 @@ function finalizeDetachedExecution(options = {}) {
|
|
|
96
98
|
) {
|
|
97
99
|
return { updated: false, reason: 'stale-attempt' };
|
|
98
100
|
}
|
|
101
|
+
const containerRemoved = normalizeBoolean(options.containerRemoved) === true;
|
|
99
102
|
if (record.status === 'executed' && record.endTime) {
|
|
100
103
|
// Already finalized (e.g. a `--status` query got there first, or the
|
|
101
|
-
// watcher ran twice after a resume).
|
|
104
|
+
// watcher ran twice after a resume). Only the removal can still be new.
|
|
105
|
+
if (containerRemoved && record.containerState !== 'removed') {
|
|
106
|
+
try {
|
|
107
|
+
const marked = store.markContainerRemoved(
|
|
108
|
+
record.uuid,
|
|
109
|
+
new Date().toISOString()
|
|
110
|
+
);
|
|
111
|
+
if (marked) {
|
|
112
|
+
return { updated: true, reason: 'container-removed', record: marked };
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Bookkeeping only: see the exit-0 contract of `main`.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
102
118
|
return { updated: false, reason: 'already-final', record };
|
|
103
119
|
}
|
|
104
120
|
|
|
@@ -171,6 +187,11 @@ function finalizeDetachedExecution(options = {}) {
|
|
|
171
187
|
if (containerError) {
|
|
172
188
|
record.options = { ...(record.options || {}), containerError };
|
|
173
189
|
}
|
|
190
|
+
if (containerRemoved) {
|
|
191
|
+
// The cleanup policy removed the container: say so, and keep its id as
|
|
192
|
+
// history (issue #225).
|
|
193
|
+
require('./container-removal').markContainerRemoved(record);
|
|
194
|
+
}
|
|
174
195
|
|
|
175
196
|
const {
|
|
176
197
|
appendLifecycle,
|
|
@@ -291,7 +312,9 @@ function reconcileFinalizedRecord(store, record) {
|
|
|
291
312
|
*/
|
|
292
313
|
function buildDetachedFinalizeSnippet(executionId, attemptNumber = null) {
|
|
293
314
|
const v = SHELL_VARS;
|
|
315
|
+
const { CONTAINER_REMOVED_ENV } = require('./container-removal');
|
|
294
316
|
return (
|
|
317
|
+
`${CONTAINER_REMOVED_ENV}="$${v.removed}" ` +
|
|
295
318
|
`${shellQuote(process.execPath)} ${shellQuote(__filename)} ` +
|
|
296
319
|
`${shellQuote(executionId)} "$${v.exit}" "$${v.oom}" ` +
|
|
297
320
|
`"$${v.started}" "$${v.finished}" "$${v.error}" "$${v.running}" ` +
|
|
@@ -301,7 +324,9 @@ function buildDetachedFinalizeSnippet(executionId, attemptNumber = null) {
|
|
|
301
324
|
|
|
302
325
|
/**
|
|
303
326
|
* Entry point used by the detached watcher:
|
|
304
|
-
* <runtime> detached-finalize.js <uuid> <exit> <oom> <started> <finished> <error> <running> <cgroup>
|
|
327
|
+
* <runtime> detached-finalize.js <uuid> <exit> <oom> <started> <finished> <error> <running> <cgroup> [attempt]
|
|
328
|
+
* with `START_INTERNAL_CONTAINER_REMOVED=true` when the watcher removed the
|
|
329
|
+
* container (issue #225).
|
|
305
330
|
* Always exits 0 — a bookkeeping failure must never turn into a visible error
|
|
306
331
|
* in a log the user is reading for the command's own output.
|
|
307
332
|
* @param {string[]} argv - Positional arguments
|
|
@@ -340,6 +365,8 @@ function main(argv) {
|
|
|
340
365
|
running,
|
|
341
366
|
cgroupMemory,
|
|
342
367
|
attemptNumber: attemptNumber || null,
|
|
368
|
+
containerRemoved:
|
|
369
|
+
process.env[require('./container-removal').CONTAINER_REMOVED_ENV],
|
|
343
370
|
});
|
|
344
371
|
} catch {
|
|
345
372
|
// Deliberately silent: see the exit-0 contract above.
|
|
@@ -381,7 +381,10 @@ function buildDetachedDockerCompletionScript(
|
|
|
381
381
|
}
|
|
382
382
|
// Everything that assumes the container has exited: cleanup, footer and
|
|
383
383
|
// finalization. Guarded as a whole by `.State.Running` below.
|
|
384
|
-
|
|
384
|
+
// `removed` tells the finalizer whether the cleanup policy removed the
|
|
385
|
+
// container (issue #225).
|
|
386
|
+
const removed = SHELL_VARS.removed;
|
|
387
|
+
const exited = [`${removed}=false`];
|
|
385
388
|
|
|
386
389
|
if (logPath) {
|
|
387
390
|
const quotedLogPath = shellQuote(logPath);
|
|
@@ -394,7 +397,7 @@ function buildDetachedDockerCompletionScript(
|
|
|
394
397
|
quotedLogPath
|
|
395
398
|
);
|
|
396
399
|
const memory = buildCgroupMemoryLogSnippet(quotedLogPath);
|
|
397
|
-
const remove = `${imageCleanup.capture}; if docker rm -f ${quotedName} >> ${quotedLogPath} 2>&1; then ${imageCleanup.remove}; fi; ${removalNote}; ${memory}`;
|
|
400
|
+
const remove = `${imageCleanup.capture}; if docker rm -f ${quotedName} >> ${quotedLogPath} 2>&1; then ${removed}=true; ${imageCleanup.remove}; fi; ${removalNote}; ${memory}`;
|
|
398
401
|
const keep = `${postMortem}; ${memory}; ${buildDockerKeptLogSnippet(containerName, quotedLogPath)}`;
|
|
399
402
|
|
|
400
403
|
const since = watcherOptions.since
|
|
@@ -444,14 +447,14 @@ function buildDetachedDockerCompletionScript(
|
|
|
444
447
|
parts.push(buildDockerStateSnippet(containerName));
|
|
445
448
|
if (policy === DOCKER_CONTAINER_CLEANUP_POLICY.ALWAYS) {
|
|
446
449
|
exited.push(
|
|
447
|
-
`${imageCleanup.capture}; if docker rm -f ${quotedName} >/dev/null 2>&1; then ${imageCleanup.remove}; fi`
|
|
450
|
+
`${imageCleanup.capture}; if docker rm -f ${quotedName} >/dev/null 2>&1; then ${removed}=true; ${imageCleanup.remove}; fi`
|
|
448
451
|
);
|
|
449
452
|
} else if (
|
|
450
453
|
policy === DOCKER_CONTAINER_CLEANUP_POLICY.DEFAULT ||
|
|
451
454
|
policy === DOCKER_CONTAINER_CLEANUP_POLICY.KEEP_ON_FAIL
|
|
452
455
|
) {
|
|
453
456
|
exited.push(
|
|
454
|
-
`if ${buildSuccessfulNonOomCondition()}; then ${imageCleanup.capture}; if docker rm -f ${quotedName} >/dev/null 2>&1; then ${imageCleanup.remove}; fi; fi`
|
|
457
|
+
`if ${buildSuccessfulNonOomCondition()}; then ${imageCleanup.capture}; if docker rm -f ${quotedName} >/dev/null 2>&1; then ${removed}=true; ${imageCleanup.remove}; fi; fi`
|
|
455
458
|
);
|
|
456
459
|
}
|
|
457
460
|
}
|
|
@@ -139,6 +139,8 @@ const SHELL_VARS = {
|
|
|
139
139
|
exitText: '__start_command_exit_text',
|
|
140
140
|
lifetime: '__start_command_lifetime',
|
|
141
141
|
running: '__start_command_running',
|
|
142
|
+
/** `true` once the watcher's own `docker rm -f` succeeded (issue #225). */
|
|
143
|
+
removed: '__start_command_removed',
|
|
142
144
|
};
|
|
143
145
|
|
|
144
146
|
/** `docker inspect -f` template collecting every post-mortem fact at once. */
|