@codeyam-editor/codeyam-editor 0.1.15-staging.g8a9ebd4 → 0.1.16-staging.ge120017
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/npm/app-port-drift.js +23 -0
- package/npm/fleet-add-failure-evidence.js +309 -0
- package/npm/fleet-add-failure.js +51 -311
- package/npm/fleet-build-data.js +47 -20
- package/npm/fleet-preview-health.js +156 -0
- package/npm/fleet-revision.js +50 -0
- package/npm/fleet-sticky-fields.js +124 -0
- package/npm/launch-config.js +43 -142
- package/npm/scenario-check.js +68 -383
- package/npm/scenario-issues.js +47 -0
- package/npm/scenario-launch.js +349 -0
- package/npm/scenario-playwright.js +42 -321
- package/npm/scenario-targeting.js +338 -0
- package/npm/vm-forwards.js +158 -0
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-Ceop11Fv.js → ContainerInspector-Cig2PDjT.js} +1 -1
- package/ui/dist/assets/{EditorShell-DjI5cEF_.js → EditorShell-DOuUJNSm.js} +76 -74
- package/ui/dist/assets/{ScenarioDataPanel-D-nWT7xD.js → ScenarioDataPanel-BRUYhl1o.js} +10 -10
- package/ui/dist/assets/{ScenarioDataPanelFullPage-ZNFZcTQU.js → ScenarioDataPanelFullPage-DjwRM8HJ.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-CcbccxCW.js +26 -0
- package/ui/dist/assets/{index-ClrdYzlw.js → index-Bkrhkuf5.js} +6 -6
- package/ui/dist/assets/index-Dw4EB88N.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/ScenarioRenderer-BW6QLGFW.js +0 -26
- package/ui/dist/assets/index-B3SMxYUB.css +0 -1
package/npm/app-port-drift.js
CHANGED
|
@@ -59,6 +59,27 @@ function parsePublishedAppPort(stdout) {
|
|
|
59
59
|
return isValidPort(port) ? port : null;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
const PUBLISHED_BEGIN = "<<<PUBLISHED_PORTS>>>";
|
|
63
|
+
const PUBLISHED_END = "<<<END_PUBLISHED_PORTS>>>";
|
|
64
|
+
|
|
65
|
+
/** The published-port read wrapped in begin/end markers, so it can ride an SSH
|
|
66
|
+
* round-trip that also prints other output (the dashboard folds it into its
|
|
67
|
+
* existing per-VM fact gather rather than paying a second SSH). */
|
|
68
|
+
function buildPublishedAppPortMarkedCommand() {
|
|
69
|
+
return `echo '${PUBLISHED_BEGIN}'; ${buildPublishedAppPortReadCommand()}; echo '${PUBLISHED_END}'`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Pull the published app port out of that marked section, or null when the
|
|
73
|
+
* section is missing or truncated — never a guess. */
|
|
74
|
+
function extractPublishedAppPort(stdout) {
|
|
75
|
+
if (typeof stdout !== "string") return null;
|
|
76
|
+
const begin = stdout.indexOf(PUBLISHED_BEGIN);
|
|
77
|
+
if (begin === -1) return null;
|
|
78
|
+
const end = stdout.indexOf(PUBLISHED_END, begin);
|
|
79
|
+
if (end === -1) return null;
|
|
80
|
+
return parsePublishedAppPort(stdout.slice(begin + PUBLISHED_BEGIN.length, end));
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
/** Parse `cloud-session-idle.sh` stdout into `idle` | `active` | null. Null
|
|
63
84
|
* (unknown) is deliberately NOT idle — the same asymmetry that script and
|
|
64
85
|
* `converge-vm.sh --converge-env` honour. */
|
|
@@ -161,6 +182,8 @@ module.exports = {
|
|
|
161
182
|
EDITOR_CONTROL_PORT,
|
|
162
183
|
buildPublishedAppPortReadCommand,
|
|
163
184
|
parsePublishedAppPort,
|
|
185
|
+
buildPublishedAppPortMarkedCommand,
|
|
186
|
+
extractPublishedAppPort,
|
|
164
187
|
parseIdleVerdict,
|
|
165
188
|
appPortDriftRemediations,
|
|
166
189
|
collectAppPortDrift,
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// npm/fleet-add-failure-evidence.js — the per-arm transcript readers and
|
|
4
|
+
// operator-facing recovery copy behind classifyAddFailure (fleet-add-failure.js).
|
|
5
|
+
// Each arm that needs more than a regex reads its facts out of the provision
|
|
6
|
+
// transcript here, and each arm whose wording varies with those facts builds its
|
|
7
|
+
// recovery line here, so the copy is asserted directly rather than only through
|
|
8
|
+
// the classifier. Pure: no I/O, never throws on empty input.
|
|
9
|
+
|
|
10
|
+
// Name the capacity GCE was short of, from whatever the stockout transcript
|
|
11
|
+
// happens to expose — for the capacity-stockout recovery line. GCE reports this
|
|
12
|
+
// inconsistently: the 2026-08-20 VM-11 transcript carried `vmType:` / `zone:`
|
|
13
|
+
// bullets under errorInfo.metadatas, while the 2026-08-18 VM-13 spelling
|
|
14
|
+
// (ZONE_RESOURCE_POOL_EXHAUSTED_WITH_DETAILS) carried neither. So every part is
|
|
15
|
+
// optional and the phrase degrades to a generic-but-true sentence rather than
|
|
16
|
+
// interpolating `undefined` into operator-facing copy. Extracted alongside
|
|
17
|
+
// nextValidActionRemedy (the same shape: a pure transcript reader that enriches
|
|
18
|
+
// one arm's recovery text) so the degradation is asserted directly.
|
|
19
|
+
//
|
|
20
|
+
// `zone:` is anchored with ^\s* so the neighbouring `zonesAvailable:` bullet —
|
|
21
|
+
// which lists the zones that DID have capacity — cannot be mistaken for the zone
|
|
22
|
+
// that failed. Never throws; returns the generic phrase for empty input.
|
|
23
|
+
function stockoutCapacityPhrase(logTail) {
|
|
24
|
+
const log = String(logTail || '');
|
|
25
|
+
const vmType = (log.match(/^\s*vmType:\s*(\S+)/m) || [])[1];
|
|
26
|
+
const zone = (log.match(/^\s*zone:\s*(\S+)/m) || [])[1];
|
|
27
|
+
return [vmType, 'capacity', zone ? `in ${zone}` : 'in the target zone']
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.join(' ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The spellings git uses for "that branch is not on the remote", each capturing
|
|
33
|
+
// the ref name. The union is not defensive padding — the two bootstrap scripts
|
|
34
|
+
// reach the remote with DIFFERENT git commands, and git words the same condition
|
|
35
|
+
// differently for each:
|
|
36
|
+
// `git fetch <url> <branch>` (clone_editor_repo, scripts/gcp-bootstrap.sh)
|
|
37
|
+
// → fatal: couldn't find remote ref <ref>
|
|
38
|
+
// `git clone --branch <b>` (clone_project, scripts/gcp-project-bootstrap.sh)
|
|
39
|
+
// → fatal: Remote branch <b> not found in upstream origin
|
|
40
|
+
// Keying on only the first — the one the 2026-08-25 transcripts happened to
|
|
41
|
+
// carry — would miss every client-side clone failure. Same union-of-spellings
|
|
42
|
+
// lesson capacity-stockout learned from GCE.
|
|
43
|
+
//
|
|
44
|
+
// Deliberately NOT included: git's `warning: Could not find remote branch …`,
|
|
45
|
+
// which is a submodule-update warning that accompanies a SUCCEEDING command.
|
|
46
|
+
// Matching it here would let an add that failed for an unrelated reason claim
|
|
47
|
+
// this category — and this arm sits above the generic one, so a false positive
|
|
48
|
+
// would suppress the true cause rather than merely add noise.
|
|
49
|
+
const MISSING_REF_PATTERNS = [
|
|
50
|
+
/couldn't find remote ref (\S+)/i,
|
|
51
|
+
/Remote branch (\S+) not found in upstream/i,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Which branch died, and which config field carried it — read from the transcript.
|
|
55
|
+
//
|
|
56
|
+
// The field matters because clientBranch and editorBranch are configured in
|
|
57
|
+
// different places: an operator who corrects only one re-issues a request that
|
|
58
|
+
// fails identically. It is derived from the log prefix of the nearest preceding
|
|
59
|
+
// marker line, because the two bootstrap scripts brand their output distinctly
|
|
60
|
+
// (`[gcp-bootstrap]` builds the editor image from the editor branch;
|
|
61
|
+
// `[project-bootstrap]` clones the client repo at the client branch). When the
|
|
62
|
+
// transcript carries neither marker the field is left null and the copy names
|
|
63
|
+
// both fields rather than guessing — the same degrade-to-true-but-generic
|
|
64
|
+
// discipline stockoutCapacityPhrase uses. Never throws; returns
|
|
65
|
+
// { ref: null, field: null } for input with no missing-ref signature.
|
|
66
|
+
function missingBranchRefFacts(logTail) {
|
|
67
|
+
const lines = String(logTail || '').split('\n');
|
|
68
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
69
|
+
let ref = null;
|
|
70
|
+
for (const re of MISSING_REF_PATTERNS) {
|
|
71
|
+
const m = lines[i].match(re);
|
|
72
|
+
if (m) { ref = m[1]; break; }
|
|
73
|
+
}
|
|
74
|
+
if (!ref) continue;
|
|
75
|
+
let field = null;
|
|
76
|
+
for (let j = i; j >= 0; j -= 1) {
|
|
77
|
+
if (/\[project-bootstrap\]/.test(lines[j])) { field = 'clientBranch'; break; }
|
|
78
|
+
if (/\[gcp-bootstrap\]/.test(lines[j])) { field = 'editorBranch'; break; }
|
|
79
|
+
}
|
|
80
|
+
return { ref, field };
|
|
81
|
+
}
|
|
82
|
+
return { ref: null, field: null };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Operator-facing recovery copy for the missing-branch-ref arm.
|
|
86
|
+
//
|
|
87
|
+
// Extracted from the arm for the same reason stockoutCapacityPhrase was — it is
|
|
88
|
+
// the one arm whose copy VARIES with what the transcript happened to expose, so
|
|
89
|
+
// pulling it out lets the wording be asserted directly instead of only through
|
|
90
|
+
// classifyAddFailure, and keeps the arm itself a two-line decision.
|
|
91
|
+
//
|
|
92
|
+
// Two properties are load-bearing and are what the tests pin:
|
|
93
|
+
// - It states the outcome FIRST and never borrows the generic arm's "a partial
|
|
94
|
+
// instance may exist — reconcile or destroy it" language. At this point git
|
|
95
|
+
// has aborted before any image build or container work, so that advice sends
|
|
96
|
+
// the operator hunting for a box that was never created — the inversion that
|
|
97
|
+
// cost the 2026-08-25 recovery its first hour.
|
|
98
|
+
// - The remedy is a DIFFERENT action, never "retry". Re-issuing the same
|
|
99
|
+
// config against a deleted ref fails identically forever, which is exactly
|
|
100
|
+
// why this category is kept out of ADD_FAILURE_RETRYABLE.
|
|
101
|
+
//
|
|
102
|
+
// `field` is optional: when the transcript carries no bootstrap marker the copy
|
|
103
|
+
// names both fields rather than guessing which one carried the dead ref.
|
|
104
|
+
// Pure; no I/O.
|
|
105
|
+
function missingBranchRefRecovery({ ref, field } = {}) {
|
|
106
|
+
const carried = field
|
|
107
|
+
? `Its ${field} still points at it`
|
|
108
|
+
: 'Its clientBranch and editorBranch both need checking';
|
|
109
|
+
return (
|
|
110
|
+
`Branch '${ref}' is not on origin any more — it was almost certainly pruned when it merged. ` +
|
|
111
|
+
'Nothing was provisioned: git aborted before any image build or container work, so there is no instance, ' +
|
|
112
|
+
`disk, NEG or backend service to clean up, and nothing to destroy. ${carried}, so retrying as-is will fail ` +
|
|
113
|
+
'identically every time. Re-add on a branch that still exists (the current fleet branch is the usual answer).'
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Facts for the wedged-insert arm: a GCE instance insert that was ACCEPTED and
|
|
118
|
+
// then never finished. Returns { wedged, code, instance } — `code` is the GCE
|
|
119
|
+
// support code when the transcript carried one, so the recovery copy can hand the
|
|
120
|
+
// operator the exact string a Support case needs instead of making them dig it out
|
|
121
|
+
// of the transcript by hand.
|
|
122
|
+
//
|
|
123
|
+
// Two spellings reach here and both are proof, so the match is a UNION:
|
|
124
|
+
// A. The raw GCE prose, which is what the 2026-09-04 VM-8 transcript carried:
|
|
125
|
+
// `Internal error. Please try again or contact Google Support. (Code: '…')`
|
|
126
|
+
// PAIRED with evidence this run actually attempted a create. The pairing is
|
|
127
|
+
// what keeps the arm narrow — the same prose surfacing from some other
|
|
128
|
+
// gcloud call is not an insert that wedged, and must not claim to be one.
|
|
129
|
+
// B. ensureInstance's own fast-fail line (npm/cloud.js), which fires when a
|
|
130
|
+
// re-add finds the slot still parked in PROVISIONING behind an operation
|
|
131
|
+
// that never reached DONE. That path exists precisely so the transcript
|
|
132
|
+
// carries a signature instead of an uninformative 2-minute timeout, so this
|
|
133
|
+
// arm keys on it directly.
|
|
134
|
+
//
|
|
135
|
+
// Deliberately NOT keyed on elapsed time. A duration heuristic ("PROVISIONING for
|
|
136
|
+
// too long") would misfire on a legitimately slow create; an INTERNAL_ERROR on an
|
|
137
|
+
// accepted insert is provable evidence, which is the same signature-over-duration
|
|
138
|
+
// discipline the capacity-stockout arm uses.
|
|
139
|
+
const WEDGED_INSERT_PROSE = /Internal error\. Please try again or contact Google Support/i;
|
|
140
|
+
const WEDGED_INSERT_FASTFAIL = /stuck in PROVISIONING behind a wedged create operation/i;
|
|
141
|
+
const WEDGED_INSERT_CREATE_ATTEMPTED = /not found\s*—\s*creating|Failed to create instance|did not reach RUNNING/i;
|
|
142
|
+
|
|
143
|
+
function wedgedInsertFacts(logTail) {
|
|
144
|
+
const log = String(logTail || '');
|
|
145
|
+
const wedged = WEDGED_INSERT_FASTFAIL.test(log)
|
|
146
|
+
|| (WEDGED_INSERT_PROSE.test(log) && WEDGED_INSERT_CREATE_ATTEMPTED.test(log));
|
|
147
|
+
if (!wedged) return { wedged: false, code: null, instance: null };
|
|
148
|
+
return {
|
|
149
|
+
wedged: true,
|
|
150
|
+
code: (log.match(/\(Code:\s*'(-?\d+)'\)/) || [])[1] || null,
|
|
151
|
+
instance: (log.match(/Checking instance\s+(\S+?)\.\.\./) || [])[1] || null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Operator-facing recovery copy for the wedged-insert arm, extracted for the same
|
|
156
|
+
// reason stockoutCapacityPhrase and missingBranchRefRecovery were: its wording
|
|
157
|
+
// varies with what the transcript exposed, so pulling it out lets the copy be
|
|
158
|
+
// asserted directly rather than only through classifyAddFailure.
|
|
159
|
+
//
|
|
160
|
+
// Three properties are load-bearing and are what the tests pin:
|
|
161
|
+
// - It never borrows the generic arm's "reconcile or destroy it" line. A delete
|
|
162
|
+
// against a wedged slot is ACCEPTED and then queues behind the wedged insert
|
|
163
|
+
// rather than clearing it, so that advice sends the operator to an action that
|
|
164
|
+
// provably does not work — the same inversion that cost the 2026-08-25
|
|
165
|
+
// missing-branch-ref recovery its first hour.
|
|
166
|
+
// - It says plainly that this is not the operator's fault and not their fix.
|
|
167
|
+
// The only thing that resolves it is GCE-side.
|
|
168
|
+
// - It names the Support code when one was captured, because that string is
|
|
169
|
+
// what a Google Cloud Support case is keyed on.
|
|
170
|
+
function wedgedInsertRecovery({ code, instance } = {}) {
|
|
171
|
+
const named = instance || 'the instance';
|
|
172
|
+
const cite = code ? ` citing operation error code ${code}` : '';
|
|
173
|
+
return (
|
|
174
|
+
`GCE accepted the create for ${named} and then never finished it: the insert stayed at status RUNNING, progress 0 ` +
|
|
175
|
+
'and eventually attached an INTERNAL_ERROR without ever reaching DONE, leaving the instance in PROVISIONING and its ' +
|
|
176
|
+
'boot disk in CREATING. Nothing on our side failed and there is nothing here to reconcile. Do NOT retry — the ' +
|
|
177
|
+
'instance name is held by the wedged operation, so a re-add cannot succeed while it is outstanding. Do NOT try to ' +
|
|
178
|
+
'destroy it either — a delete is accepted but queues behind the wedged insert instead of clearing it. This needs ' +
|
|
179
|
+
`GCE-side resolution: open a Google Cloud Support case${cite}. If you need the slot before Google releases it, add ` +
|
|
180
|
+
'on a different VM number.'
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Facts for the iap-not-ready arm: waitForIapReady (npm/cloud.js) threw its
|
|
185
|
+
// readiness timeout. Returns { timedOut, instance, budgetSecs, cause } — `cause` is
|
|
186
|
+
// the label waitForIapReady derived from its probe stderr (null when none of its
|
|
187
|
+
// signatures matched, or on a transcript from before the label existed), so the
|
|
188
|
+
// recovery copy can name WHICH wait was blown instead of only `return code [255]`.
|
|
189
|
+
//
|
|
190
|
+
// The signature is emitted from exactly one place, and that place runs before any
|
|
191
|
+
// bootstrap work, which is what lets this arm assert the instance is bare.
|
|
192
|
+
const IAP_NOT_READY = /IAP did not become ready for (\S+) within (\d+)s/;
|
|
193
|
+
|
|
194
|
+
function iapNotReadyFacts(logTail) {
|
|
195
|
+
const m = String(logTail || '').match(IAP_NOT_READY);
|
|
196
|
+
if (!m) return { timedOut: false, instance: null, budgetSecs: null, cause: null };
|
|
197
|
+
const cause = (String(logTail).match(/likely cause: ([^;)]+)/) || [])[1] || null;
|
|
198
|
+
return { timedOut: true, instance: m[1], budgetSecs: Number(m[2]), cause: cause && cause.trim() };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Operator-facing recovery copy for the iap-not-ready arm. It states what the
|
|
202
|
+
// generic arm cannot: the instance EXISTS and is BARE. "A partial instance may
|
|
203
|
+
// exist" is what steered VM-4's operator toward destroy + re-add (15-25 minutes)
|
|
204
|
+
// when Retry resumes on the same box in seconds — and it left nothing on the card
|
|
205
|
+
// saying a RUNNING, billing instance was sitting there.
|
|
206
|
+
function iapNotReadyRecovery({ instance, budgetSecs, cause } = {}) {
|
|
207
|
+
const named = instance || 'The instance';
|
|
208
|
+
const within = budgetSecs ? ` within ${budgetSecs}s` : '';
|
|
209
|
+
const why = cause ? ` (likely cause: ${cause})` : '';
|
|
210
|
+
return (
|
|
211
|
+
`${named} was created and is RUNNING, but SSH over IAP did not answer${within}${why}. This is usually a transient ` +
|
|
212
|
+
'control-plane race. Nothing was installed on it — the wait runs before any bootstrap step — so it is a bare box and ' +
|
|
213
|
+
'there is nothing partial to reconcile. Retry add resumes on this same instance rather than creating a second one. ' +
|
|
214
|
+
'Until you retry or destroy it, the instance keeps running and billing.'
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Facts for the streamed-library-missing arm: a streamed bootstrap script sourced
|
|
219
|
+
// a `$HOME`-rooted `lib/…` file that was never shipped to the VM, and bash
|
|
220
|
+
// reported `<script>: line N: <path>: No such file or directory`. This took ALL
|
|
221
|
+
// fleet provisioning down twice (lib/npm-pin.sh 2026-09-04, lib/render-cloud-
|
|
222
|
+
// compose.sh 2026-09-18). Returns { missing, script, library } — `script` is the
|
|
223
|
+
// basename of the aborting script, `library` the `lib/…` path relative to $HOME,
|
|
224
|
+
// which is how PROJECT_BOOTSTRAP_LIBS (npm/cloud.js) names its entries.
|
|
225
|
+
const STREAMED_LIBRARY_MISSING = /(\S+): line \d+: \/(?:home\/[^/\s]+|root)\/(lib\/[^\s:]+): No such file or directory/;
|
|
226
|
+
|
|
227
|
+
function streamedLibraryMissingFacts(logTail) {
|
|
228
|
+
const m = String(logTail || '').match(STREAMED_LIBRARY_MISSING);
|
|
229
|
+
if (!m) return { missing: false, script: null, library: null };
|
|
230
|
+
return { missing: true, script: m[1].split('/').pop(), library: m[2] };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Did cloud:up report that it deleted the instance it created this run? The abort
|
|
234
|
+
// path (npm/cloud.js) prints the teardown marker BEFORE it issues the delete, and
|
|
235
|
+
// prints a `during abort teardown` warning when that delete fails — so the marker
|
|
236
|
+
// alone is a claim of intent, and only the marker WITHOUT the warning is evidence
|
|
237
|
+
// that no orphan was left behind.
|
|
238
|
+
function cleanTeardownReported(logTail) {
|
|
239
|
+
const log = String(logTail || '');
|
|
240
|
+
return /tearing down the VM cloud:up created this run \(no orphan left behind\)/.test(log)
|
|
241
|
+
&& !/during abort teardown/.test(log);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Operator-facing recovery copy for the streamed-library-missing arm. It names
|
|
245
|
+
// what to fix — register the library so it is streamed — and, when the transcript
|
|
246
|
+
// proves a clean teardown, says there is no orphan to hunt for. Every add on this
|
|
247
|
+
// branch fails identically until the registry is fixed, so it never offers Retry.
|
|
248
|
+
function streamedLibraryMissingRecovery({ script, library, cleanTeardown } = {}) {
|
|
249
|
+
const lib = library || 'a lib/ file';
|
|
250
|
+
const from = script ? ` sourced by ${script}` : '';
|
|
251
|
+
const orphan = cleanTeardown
|
|
252
|
+
? 'cloud:up already deleted the VM it created, so there is nothing to reconcile or destroy.'
|
|
253
|
+
: 'The transcript does not confirm the VM was deleted — check whether a partial instance exists before re-adding.';
|
|
254
|
+
return (
|
|
255
|
+
`${lib}${from} was never shipped to the VM, so the bootstrap aborted. Add it to PROJECT_BOOTSTRAP_LIBS in npm/cloud.js, ` +
|
|
256
|
+
`push, and re-add — every add from this branch fails the same way until then. ${orphan}`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Did the OPERATOR HOST run out of filesystem? Node spells it `ENOSPC: no space
|
|
261
|
+
// left on device`, the shell `No space left on device`, gcloud `OSError: [Errno
|
|
262
|
+
// 28] No space left on device` — matched on the message, never on the errno
|
|
263
|
+
// number, which means something else on other platforms. A transcript that says a
|
|
264
|
+
// script "failed on the VM" is quoting the VM's own log tail (npm/cloud.js prints
|
|
265
|
+
// that header above it), so an ENOSPC there is the VM's disk, not the host's, and
|
|
266
|
+
// stays with the generic arm.
|
|
267
|
+
const HOST_DISK_EXHAUSTED = /ENOSPC: no space left on device|No space left on device/i;
|
|
268
|
+
|
|
269
|
+
function hostDiskExhausted(logTail) {
|
|
270
|
+
const log = String(logTail || '');
|
|
271
|
+
return HOST_DISK_EXHAUSTED.test(log) && !/failed on the VM/i.test(log);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Operator-facing recovery copy for the host-disk-exhausted arm. The ENOSPC fires
|
|
275
|
+
// on the operator host before `instances create`, so nothing exists on GCP; the
|
|
276
|
+
// fix is freeing the host's filesystem, which every add needs first.
|
|
277
|
+
function hostDiskExhaustedRecovery() {
|
|
278
|
+
return (
|
|
279
|
+
'The operator host ran out of filesystem space (bytes or inodes) before anything was created, so there is no VM to ' +
|
|
280
|
+
'reconcile or destroy. Prune old date directories under /root/.config/gcloud/logs — gcloud writes one log per ' +
|
|
281
|
+
'invocation and is the usual cause — and confirm with `df -i` and `df -h`. Every add fails the same way until then.'
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Operator-facing recovery copy for the generic cloud-up-failed arm. "A partial
|
|
286
|
+
// instance may exist" is only true when the transcript does not show cloud:up
|
|
287
|
+
// deleting what it created; saying it regardless sends the operator hunting for
|
|
288
|
+
// an orphan that is not there.
|
|
289
|
+
function cloudUpFailedRecovery({ cleanTeardown } = {}) {
|
|
290
|
+
return cleanTeardown
|
|
291
|
+
? 'cloud:up failed during provisioning and deleted the VM it created, so nothing was left behind. Read the transcript for the cause before re-adding.'
|
|
292
|
+
: 'cloud:up failed during provisioning, so a partial instance may exist. Reconcile or destroy this VM before re-adding.';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
module.exports = {
|
|
296
|
+
stockoutCapacityPhrase,
|
|
297
|
+
missingBranchRefFacts,
|
|
298
|
+
missingBranchRefRecovery,
|
|
299
|
+
wedgedInsertFacts,
|
|
300
|
+
wedgedInsertRecovery,
|
|
301
|
+
iapNotReadyFacts,
|
|
302
|
+
iapNotReadyRecovery,
|
|
303
|
+
streamedLibraryMissingFacts,
|
|
304
|
+
cleanTeardownReported,
|
|
305
|
+
streamedLibraryMissingRecovery,
|
|
306
|
+
hostDiskExhausted,
|
|
307
|
+
hostDiskExhaustedRecovery,
|
|
308
|
+
cloudUpFailedRecovery,
|
|
309
|
+
};
|