@ornncompute/cli 0.2.23 → 1.0.25
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/README.md +304 -420
- package/package.json +12 -6
- package/src/admin-capacity-reservations.mjs +280 -0
- package/src/admin-command-handlers.mjs +57 -0
- package/src/admin-enrollment.mjs +680 -0
- package/src/admin-mutations.mjs +187 -0
- package/src/admin-node-diagnostics.mjs +412 -0
- package/src/admin-node-lifecycle.mjs +908 -0
- package/src/admin-shell.mjs +322 -0
- package/src/api-client.mjs +81 -28
- package/src/atomic-output.mjs +112 -0
- package/src/auth-store.mjs +182 -11
- package/src/capability-catalog.mjs +7 -0
- package/src/catalog-shell.mjs +1350 -0
- package/src/cli.mjs +123 -10266
- package/src/connect-mode-handlers.mjs +170 -0
- package/src/device-auth.mjs +178 -61
- package/src/evidence-commands.mjs +1340 -0
- package/src/external-bucket-commands.mjs +1374 -0
- package/src/interaction-runtime.mjs +310 -0
- package/src/kubernetes-commands.mjs +2229 -0
- package/src/launch-mode-handlers.mjs +119 -0
- package/src/machine-reference.mjs +277 -0
- package/src/mount-commands.mjs +1146 -0
- package/src/session-commands.mjs +565 -0
- package/src/slurm-commands.mjs +263 -0
- package/src/startup.mjs +34 -11
- package/src/storage-commands.mjs +1144 -0
- package/src/update.mjs +172 -24
- package/src/vm-commands.mjs +4273 -0
- package/src/vpn-secret-output.mjs +150 -0
- package/src/wait-runtime.mjs +231 -0
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +126 -0
- package/vendor/capabilities/capabilities/admin-enrollment.js +321 -0
- package/vendor/capabilities/capabilities/admin-node-diagnostics.js +88 -0
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +235 -0
- package/vendor/capabilities/capabilities/admin.js +89 -0
- package/vendor/capabilities/capabilities/capacity-offers.js +368 -0
- package/vendor/capabilities/capabilities/cli.js +156 -0
- package/vendor/capabilities/capabilities/clusters.js +150 -1
- package/vendor/capabilities/capabilities/compute.js +522 -0
- package/vendor/capabilities/capabilities/evidence.js +145 -0
- package/vendor/capabilities/capabilities/external-buckets.js +191 -0
- package/vendor/capabilities/capabilities/fleet.js +21 -5
- package/vendor/capabilities/capabilities/identity.js +114 -3
- package/vendor/capabilities/capabilities/listings.js +14 -8
- package/vendor/capabilities/capabilities/mounts.js +176 -0
- package/vendor/capabilities/capabilities/networks.js +1257 -0
- package/vendor/capabilities/capabilities/observability.js +38 -8
- package/vendor/capabilities/capabilities/tokens.js +20 -8
- package/vendor/capabilities/capabilities/volumes.js +178 -0
- package/vendor/capabilities/capability.js +19 -13
- package/vendor/capabilities/cli-catalog.js +65 -0
- package/vendor/capabilities/cli-migrations.js +288 -0
- package/vendor/capabilities/cli.js +7 -0
- package/vendor/capabilities/software-catalog.js +36 -0
- package/vendor/capabilities/spec-catalog.js +2 -2
- package/src/catalog-dispatch.mjs +0 -605
- package/src/color.mjs +0 -71
- package/vendor/capabilities/capabilities/clusters.d.ts +0 -2
- package/vendor/capabilities/capabilities/fleet.d.ts +0 -8
- package/vendor/capabilities/capabilities/identity.d.ts +0 -3
- package/vendor/capabilities/capabilities/listings.d.ts +0 -51
- package/vendor/capabilities/capabilities/observability.d.ts +0 -5
- package/vendor/capabilities/capabilities/staff-access.d.ts +0 -29
- package/vendor/capabilities/capabilities/staff-access.js +0 -278
- package/vendor/capabilities/capabilities/staff-commerce.d.ts +0 -160
- package/vendor/capabilities/capabilities/staff-commerce.js +0 -689
- package/vendor/capabilities/capabilities/staff-fleet.d.ts +0 -140
- package/vendor/capabilities/capabilities/staff-fleet.js +0 -538
- package/vendor/capabilities/capabilities/staff-gtm.d.ts +0 -142
- package/vendor/capabilities/capabilities/staff-gtm.js +0 -521
- package/vendor/capabilities/capabilities/staff-shared.d.ts +0 -47
- package/vendor/capabilities/capabilities/staff-shared.js +0 -98
- package/vendor/capabilities/capabilities/tokens.d.ts +0 -10
- package/vendor/capabilities/capabilities/users.d.ts +0 -2
- package/vendor/capabilities/capability.d.ts +0 -93
- package/vendor/capabilities/catalog.d.ts +0 -3
- package/vendor/capabilities/catalog.js +0 -26
- package/vendor/capabilities/index.d.ts +0 -16
- package/vendor/capabilities/index.js +0 -16
- package/vendor/capabilities/names.d.ts +0 -6
- package/vendor/capabilities/role.d.ts +0 -6
- package/vendor/capabilities/role.js +0 -17
- package/vendor/capabilities/spec-catalog.d.ts +0 -78
|
@@ -0,0 +1,1340 @@
|
|
|
1
|
+
import { clusterFactLabel, reservationFactsForCluster } from "./kubernetes-commands.mjs";
|
|
2
|
+
import { listOwnedActiveMachines, resolveMachineReference } from "./machine-reference.mjs";
|
|
3
|
+
import { machineReachability } from "./vm-commands.mjs";
|
|
4
|
+
|
|
5
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6
|
+
const DEFAULT_HISTORY_MS = 15 * 60 * 1000;
|
|
7
|
+
const DEFAULT_LOG_LIMIT = 200;
|
|
8
|
+
const DEFAULT_MAX_POINTS = 240;
|
|
9
|
+
const DEFAULT_RETRY_WINDOW_MS = 5_000;
|
|
10
|
+
const DEFAULT_FOLLOW_INTERVAL_MS = 5_000;
|
|
11
|
+
const MAX_PAGE_SIZE = 500;
|
|
12
|
+
const MAX_RETRIES = 3;
|
|
13
|
+
const SAFE_METRICS = new Set([
|
|
14
|
+
"cpu_utilization",
|
|
15
|
+
"gpu_utilization",
|
|
16
|
+
"gpu_memory_utilization",
|
|
17
|
+
"gpu_power_w",
|
|
18
|
+
"gpu_tflops",
|
|
19
|
+
"memory_utilization",
|
|
20
|
+
"storage_utilization",
|
|
21
|
+
]);
|
|
22
|
+
const TERMINAL_MACHINE_STATES = new Set(["deleted", "ended", "released", "terminated"]);
|
|
23
|
+
const UNSAFE_LOG_MESSAGE = [
|
|
24
|
+
/\b(?:operator(?:name|id|_name|_id)?|provider(?:name|id|_name|_id)?|site_operator|supplier(?:name|id|_name|_id)?)\s*[:=]/i,
|
|
25
|
+
/\b(?:authorization|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|password|secret|private[_ -]?key)\s*[:=]/i,
|
|
26
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
|
|
27
|
+
/\/(?:internal|v1\/(?:orchestrator|observability|cli\/operator))(?:\/|\b)/i,
|
|
28
|
+
/\b(?:staff_activity|staff activity|internal route|route)\s*[:=]/i,
|
|
29
|
+
];
|
|
30
|
+
const UNSAFE_KEYS = new Set([
|
|
31
|
+
"authorization",
|
|
32
|
+
"clientsecret",
|
|
33
|
+
"operator",
|
|
34
|
+
"operatorid",
|
|
35
|
+
"operatorname",
|
|
36
|
+
"password",
|
|
37
|
+
"paymentsecret",
|
|
38
|
+
"privatekey",
|
|
39
|
+
"provider",
|
|
40
|
+
"providerid",
|
|
41
|
+
"providername",
|
|
42
|
+
"refreshtoken",
|
|
43
|
+
"siteoperator",
|
|
44
|
+
"siteoperatorid",
|
|
45
|
+
"siteoperatorname",
|
|
46
|
+
"staffactivity",
|
|
47
|
+
"stripeclientsecret",
|
|
48
|
+
"supplier",
|
|
49
|
+
"supplierid",
|
|
50
|
+
"suppliername",
|
|
51
|
+
"token",
|
|
52
|
+
]);
|
|
53
|
+
const STABLE_SOURCE_CODES = new Set([
|
|
54
|
+
"invalid_logs_response",
|
|
55
|
+
"invalid_metrics_response",
|
|
56
|
+
"logs_rejected",
|
|
57
|
+
"logs_unavailable",
|
|
58
|
+
"metrics_rejected",
|
|
59
|
+
"metrics_unavailable",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
function record(value) {
|
|
63
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function text(value) {
|
|
67
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizedKey(value) {
|
|
71
|
+
return String(value)
|
|
72
|
+
.replaceAll(/[^a-z0-9]/gi, "")
|
|
73
|
+
.toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function containsUnsafeFields(value) {
|
|
77
|
+
if (Array.isArray(value)) return value.some(containsUnsafeFields);
|
|
78
|
+
if (!value || typeof value !== "object") return false;
|
|
79
|
+
return Object.entries(value).some(([key, child]) => {
|
|
80
|
+
const normalized = normalizedKey(key);
|
|
81
|
+
return (
|
|
82
|
+
UNSAFE_KEYS.has(normalized) ||
|
|
83
|
+
normalized.includes("secret") ||
|
|
84
|
+
normalized.includes("token") ||
|
|
85
|
+
normalized.includes("password") ||
|
|
86
|
+
normalized.includes("privatekey") ||
|
|
87
|
+
normalized.includes("apikey") ||
|
|
88
|
+
normalized.includes("accesskey") ||
|
|
89
|
+
normalized.includes("credential") ||
|
|
90
|
+
containsUnsafeFields(child)
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function machineId(value) {
|
|
96
|
+
const source = record(value);
|
|
97
|
+
return text(source.id) ?? text(source.machine_id) ?? text(source.instance_id);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function reservationId(value) {
|
|
101
|
+
const source = record(value);
|
|
102
|
+
return text(source.reservation_id) ?? text(source.reservationId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hostname(value) {
|
|
106
|
+
const source = record(value);
|
|
107
|
+
return text(source.hostname) ?? text(source.machine_hostname) ?? text(source.site_label);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function stateToken(value) {
|
|
111
|
+
return (
|
|
112
|
+
(text(value) ?? "unknown")
|
|
113
|
+
.toLowerCase()
|
|
114
|
+
.replaceAll(/[^a-z0-9]+/g, "_")
|
|
115
|
+
.replaceAll(/^_+|_+$/g, "") || "unknown"
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function integerOption(interaction, value, name, fallback, maximum = MAX_PAGE_SIZE) {
|
|
120
|
+
if (value === undefined) return fallback;
|
|
121
|
+
if (!/^\d+$/.test(String(value))) {
|
|
122
|
+
interaction.usage(`--${name} must be an integer from 1 to ${maximum}.`, {
|
|
123
|
+
code: "invalid_input",
|
|
124
|
+
field: name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const parsed = Number(value);
|
|
128
|
+
if (parsed < 1 || parsed > maximum) {
|
|
129
|
+
interaction.usage(`--${name} must be an integer from 1 to ${maximum}.`, {
|
|
130
|
+
code: "invalid_input",
|
|
131
|
+
field: name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return parsed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function durationMilliseconds(interaction, value, name) {
|
|
138
|
+
if (value === undefined) return null;
|
|
139
|
+
const match = String(value)
|
|
140
|
+
.trim()
|
|
141
|
+
.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)$/i);
|
|
142
|
+
if (!match) {
|
|
143
|
+
interaction.usage(`--${name} must be a positive duration such as 500ms, 30s, 5m, or 1h.`, {
|
|
144
|
+
code: "invalid_input",
|
|
145
|
+
field: name,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const unit = match[2].toLowerCase();
|
|
149
|
+
const multiplier = unit === "ms" ? 1 : unit === "s" ? 1_000 : unit === "m" ? 60_000 : 3_600_000;
|
|
150
|
+
const milliseconds = Number(match[1]) * multiplier;
|
|
151
|
+
if (!Number.isFinite(milliseconds) || milliseconds < 1) {
|
|
152
|
+
interaction.usage(`--${name} must be a positive duration.`, {
|
|
153
|
+
code: "invalid_input",
|
|
154
|
+
field: name,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return Math.round(milliseconds);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function nowMilliseconds(now) {
|
|
161
|
+
const value = typeof now === "function" ? now() : Date.now();
|
|
162
|
+
if (value instanceof Date) return value.getTime();
|
|
163
|
+
return typeof value === "number" && Number.isFinite(value) ? value : Date.now();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function timestamp(interaction, value, field) {
|
|
167
|
+
const source = String(value);
|
|
168
|
+
const rfc3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
|
|
169
|
+
const milliseconds = rfc3339.test(source) ? Date.parse(source) : Number.NaN;
|
|
170
|
+
if (!Number.isFinite(milliseconds)) {
|
|
171
|
+
interaction.usage(`--${field} must be an RFC 3339 timestamp.`, {
|
|
172
|
+
code: "invalid_input",
|
|
173
|
+
field,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return milliseconds;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function evidenceWindow(input, interaction, now, { follow = false, logsOnly = false } = {}) {
|
|
180
|
+
if (input.before !== undefined && !logsOnly) {
|
|
181
|
+
interaction.usage("--before is available only for a logs command.", {
|
|
182
|
+
code: "invalid_input",
|
|
183
|
+
field: "before",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (input.before !== undefined && (input.start !== undefined || input.end !== undefined)) {
|
|
187
|
+
interaction.usage("--before cannot be combined with --start or --end.", {
|
|
188
|
+
code: "invalid_input",
|
|
189
|
+
field: "before",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if ((input.start === undefined) !== (input.end === undefined)) {
|
|
193
|
+
interaction.usage("--start and --end must be supplied together.", {
|
|
194
|
+
code: "metric_window_incomplete",
|
|
195
|
+
field: input.start === undefined ? "start" : "end",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (follow && input.end !== undefined) {
|
|
199
|
+
interaction.usage("--end cannot be combined with --follow.", {
|
|
200
|
+
code: "invalid_input",
|
|
201
|
+
field: "end",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const effectiveNow = nowMilliseconds(now);
|
|
206
|
+
const endMs =
|
|
207
|
+
input.before !== undefined
|
|
208
|
+
? timestamp(interaction, input.before, "before")
|
|
209
|
+
: input.end !== undefined
|
|
210
|
+
? timestamp(interaction, input.end, "end")
|
|
211
|
+
: effectiveNow;
|
|
212
|
+
const startMs =
|
|
213
|
+
input.start !== undefined
|
|
214
|
+
? timestamp(interaction, input.start, "start")
|
|
215
|
+
: endMs - DEFAULT_HISTORY_MS;
|
|
216
|
+
if (startMs >= endMs) {
|
|
217
|
+
interaction.usage("The evidence window requires --start before --end.", {
|
|
218
|
+
code: "invalid_metric_window",
|
|
219
|
+
field: "start",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
start: new Date(startMs).toISOString(),
|
|
224
|
+
end: new Date(endMs).toISOString(),
|
|
225
|
+
startMs,
|
|
226
|
+
endMs,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function lifecycle(value, kind) {
|
|
231
|
+
const source = record(value);
|
|
232
|
+
const state = stateToken(source.state ?? source.status);
|
|
233
|
+
if (["deleted", "ended", "released", "terminated", "torn_down"].includes(state)) {
|
|
234
|
+
return { class: "ended", phase: kind === "cluster" ? "torn_down" : "ended" };
|
|
235
|
+
}
|
|
236
|
+
if (["failed", "error", "unavailable"].includes(state)) {
|
|
237
|
+
return { class: "failed", phase: "failed" };
|
|
238
|
+
}
|
|
239
|
+
if (["active", "online", "ready", "running", "provisioned"].includes(state)) {
|
|
240
|
+
return { class: "ready", phase: kind === "cluster" ? "access_ready" : "active" };
|
|
241
|
+
}
|
|
242
|
+
if (["creating", "pending", "provisioning", "starting", "switching"].includes(state)) {
|
|
243
|
+
return { class: "working", phase: "provisioning" };
|
|
244
|
+
}
|
|
245
|
+
return { class: "pending", phase: "unknown" };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function reachability(value) {
|
|
249
|
+
const source = record(value);
|
|
250
|
+
const explicit = source.reachable;
|
|
251
|
+
if (explicit === true) return { state: "reachable", observed_at: null };
|
|
252
|
+
if (explicit === false) return { state: "unreachable", observed_at: null };
|
|
253
|
+
const candidate = record(source.reachability);
|
|
254
|
+
const offGrid = record(source.off_grid_reachability);
|
|
255
|
+
const state = stateToken(candidate.state ?? candidate.status ?? offGrid.state ?? offGrid.status);
|
|
256
|
+
const phase = stateToken(candidate.phase ?? offGrid.phase);
|
|
257
|
+
const observedAt =
|
|
258
|
+
text(candidate.observed_at) ?? text(candidate.checked_at) ?? text(offGrid.last_checked_at);
|
|
259
|
+
if (["reachable", "live", "online"].includes(state) || phase === "ssh_ready")
|
|
260
|
+
return { state: "reachable", observed_at: observedAt };
|
|
261
|
+
if (
|
|
262
|
+
["unreachable", "offline", "off_grid"].includes(state) ||
|
|
263
|
+
["ssh_unreachable", "unreachable"].includes(phase)
|
|
264
|
+
)
|
|
265
|
+
return { state: "unreachable", observed_at: observedAt };
|
|
266
|
+
const inferred = machineReachability(source);
|
|
267
|
+
if (inferred.class === "ready") return { state: "reachable", observed_at: observedAt };
|
|
268
|
+
if (inferred.class === "failed") return { state: "unreachable", observed_at: observedAt };
|
|
269
|
+
return { state: "unknown", observed_at: observedAt };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function authoritativeHealth(value) {
|
|
273
|
+
const source = record(value);
|
|
274
|
+
const assessment = record(source.health_assessment ?? source.authoritative_health);
|
|
275
|
+
const explicitHealth = record(source.health);
|
|
276
|
+
const authoritative = Object.keys(assessment).length
|
|
277
|
+
? assessment
|
|
278
|
+
: explicitHealth.authoritative === true
|
|
279
|
+
? explicitHealth
|
|
280
|
+
: {};
|
|
281
|
+
const state = stateToken(authoritative.state ?? authoritative.status);
|
|
282
|
+
return {
|
|
283
|
+
state: ["healthy", "degraded", "unhealthy"].includes(state) ? state : "unknown",
|
|
284
|
+
assessed_at: text(authoritative.assessed_at) ?? text(authoritative.observed_at),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function projectMachine(value, fallbackReservationId = null) {
|
|
289
|
+
const source = record(record(value).machine ?? value);
|
|
290
|
+
const id = machineId(source);
|
|
291
|
+
const reservation = reservationId(source) ?? fallbackReservationId;
|
|
292
|
+
if (!id || !reservation) return null;
|
|
293
|
+
const mode = stateToken(source.access_mode ?? source.mode);
|
|
294
|
+
const accessMode =
|
|
295
|
+
mode === "bare_metal"
|
|
296
|
+
? "bare-metal"
|
|
297
|
+
: ["virtual_machine", "vm"].includes(mode)
|
|
298
|
+
? "vm"
|
|
299
|
+
: "unknown";
|
|
300
|
+
return {
|
|
301
|
+
id,
|
|
302
|
+
hostname: hostname(source),
|
|
303
|
+
reservation_id: reservation,
|
|
304
|
+
access_mode: accessMode,
|
|
305
|
+
lifecycle: lifecycle(source, "machine"),
|
|
306
|
+
reachability: reachability(source),
|
|
307
|
+
health: authoritativeHealth(source),
|
|
308
|
+
source,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function memberLifecycle(value) {
|
|
313
|
+
const state = stateToken(value);
|
|
314
|
+
if (["ready", "active", "running", "joined", "member"].includes(state)) {
|
|
315
|
+
return { class: "ready", phase: "member" };
|
|
316
|
+
}
|
|
317
|
+
if (["failed", "error", "cleanup_failed", "join_failed"].includes(state)) {
|
|
318
|
+
return { class: "failed", phase: "member_failed" };
|
|
319
|
+
}
|
|
320
|
+
if (["deleted", "removed", "detached"].includes(state)) {
|
|
321
|
+
return { class: "ended", phase: "removed" };
|
|
322
|
+
}
|
|
323
|
+
return { class: "working", phase: "membership_pending" };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function clusterLifecycle(source, machines) {
|
|
327
|
+
const state = stateToken(source.state ?? source.status);
|
|
328
|
+
const credentialState = stateToken(source.credential_state);
|
|
329
|
+
if (["deleted", "ended", "released", "terminated", "torn_down"].includes(state)) {
|
|
330
|
+
return { class: "ended", phase: "torn_down" };
|
|
331
|
+
}
|
|
332
|
+
if (state === "deleting") return { class: "working", phase: "tearing_down" };
|
|
333
|
+
if (["failed", "error", "unavailable"].includes(state)) {
|
|
334
|
+
return { class: machines.length ? "partial" : "failed", phase: "provisioning_failed" };
|
|
335
|
+
}
|
|
336
|
+
if (machines.some((machine) => machine.lifecycle.class === "failed")) {
|
|
337
|
+
return { class: "partial", phase: "member_failed" };
|
|
338
|
+
}
|
|
339
|
+
if (state === "active" && ["ready", "active", "available"].includes(credentialState)) {
|
|
340
|
+
return { class: "ready", phase: "access_ready" };
|
|
341
|
+
}
|
|
342
|
+
if (state === "active") return { class: "working", phase: "credentials_pending" };
|
|
343
|
+
if (["provisioning", "creating", "pending"].includes(state)) {
|
|
344
|
+
return { class: "working", phase: "provisioning" };
|
|
345
|
+
}
|
|
346
|
+
return { class: "pending", phase: "unknown" };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function projectCluster(value) {
|
|
350
|
+
const source = record(value);
|
|
351
|
+
const id = text(source.id) ?? text(source.cluster_id);
|
|
352
|
+
const reservation = reservationId(source);
|
|
353
|
+
if (!id || !reservation) return null;
|
|
354
|
+
const rawMachines = Array.isArray(source.machines)
|
|
355
|
+
? source.machines
|
|
356
|
+
: Array.isArray(source.members)
|
|
357
|
+
? source.members
|
|
358
|
+
: [];
|
|
359
|
+
const machines = rawMachines.flatMap((member) => {
|
|
360
|
+
const source = record(member);
|
|
361
|
+
const id = machineId(source) ?? text(source.node_id);
|
|
362
|
+
return id
|
|
363
|
+
? [
|
|
364
|
+
{
|
|
365
|
+
id,
|
|
366
|
+
hostname: hostname(source),
|
|
367
|
+
lifecycle: memberLifecycle(source.state ?? source.membership_state),
|
|
368
|
+
},
|
|
369
|
+
]
|
|
370
|
+
: [];
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
id,
|
|
374
|
+
reservation_id: reservation,
|
|
375
|
+
access_mode: ["kubernetes", "slurm"].includes(stateToken(source.access_mode))
|
|
376
|
+
? stateToken(source.access_mode)
|
|
377
|
+
: "unknown",
|
|
378
|
+
lifecycle: clusterLifecycle(source, machines),
|
|
379
|
+
gpu_type: text(source.gpu_type ?? source.hardware),
|
|
380
|
+
reachability: reachability(source),
|
|
381
|
+
health: authoritativeHealth(source),
|
|
382
|
+
machines,
|
|
383
|
+
source,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function payloadRows(payload, key) {
|
|
388
|
+
if (Array.isArray(payload)) return payload;
|
|
389
|
+
const rows = record(payload)[key];
|
|
390
|
+
return Array.isArray(rows) ? rows : null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function activeMachines(fetch, interaction) {
|
|
394
|
+
const rows = await listOwnedActiveMachines({ fetch }, interaction);
|
|
395
|
+
return rows.flatMap((row) => {
|
|
396
|
+
const projected = projectMachine(row);
|
|
397
|
+
return projected ? [projected] : [];
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function clusters(fetch, interaction) {
|
|
402
|
+
const payload = await fetch("/v1/workflows/compute/clusters");
|
|
403
|
+
const rows = payloadRows(payload, "clusters");
|
|
404
|
+
if (!rows)
|
|
405
|
+
interaction.error("The gateway returned an invalid cluster collection.", {
|
|
406
|
+
code: "invalid_gateway_response",
|
|
407
|
+
});
|
|
408
|
+
return rows
|
|
409
|
+
.flatMap((row) => {
|
|
410
|
+
const projected = projectCluster(row);
|
|
411
|
+
return projected ? [projected] : [];
|
|
412
|
+
})
|
|
413
|
+
.sort(
|
|
414
|
+
(left, right) =>
|
|
415
|
+
left.reservation_id.localeCompare(right.reservation_id) || left.id.localeCompare(right.id)
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function resourceChoice(resource) {
|
|
420
|
+
if (resource.kind === "machine") {
|
|
421
|
+
return {
|
|
422
|
+
label: `VM/Bare Metal · ${resource.machine.hostname ?? "Unnamed machine"} · ${resource.machine.lifecycle.class}`,
|
|
423
|
+
value: `machine:${resource.machine.id}`,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
label: clusterFactLabel(resource.cluster, resource.reservation_facts),
|
|
428
|
+
value: `cluster:${resource.cluster.id}`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function resolveResource(command, input, fetch, interaction) {
|
|
433
|
+
const expectedKind = command.startsWith("machines.")
|
|
434
|
+
? "machine"
|
|
435
|
+
: command.startsWith("clusters.")
|
|
436
|
+
? "cluster"
|
|
437
|
+
: null;
|
|
438
|
+
const positionals = input.$positionals ?? [];
|
|
439
|
+
if (positionals.length > 1) interaction.usage("Supply exactly one machine or cluster reference.");
|
|
440
|
+
const reference = text(positionals[0]);
|
|
441
|
+
|
|
442
|
+
if (reference && expectedKind !== "cluster") {
|
|
443
|
+
const source = await resolveMachineReference(reference, { fetch }, interaction, {
|
|
444
|
+
allowMiss: expectedKind === null,
|
|
445
|
+
});
|
|
446
|
+
if (source) {
|
|
447
|
+
const machine = projectMachine(source);
|
|
448
|
+
if (!machine) {
|
|
449
|
+
interaction.error("The gateway returned an invalid machine.", {
|
|
450
|
+
code: "invalid_gateway_response",
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return { kind: "machine", machine };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (reference && UUID.test(reference) && expectedKind !== "machine") {
|
|
457
|
+
const matches = (await clusters(fetch, interaction)).filter(
|
|
458
|
+
(cluster) => cluster.id === reference
|
|
459
|
+
);
|
|
460
|
+
if (matches.length === 1) return { kind: "cluster", cluster: matches[0] };
|
|
461
|
+
if (matches.length > 1)
|
|
462
|
+
interaction.error("The gateway returned duplicate canonical cluster IDs.", {
|
|
463
|
+
code: "cluster_ambiguous",
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
if (reference && expectedKind === "cluster") {
|
|
467
|
+
interaction.usage(
|
|
468
|
+
"Clusters are referenced only by canonical ID or an invocation-local guided choice.",
|
|
469
|
+
{
|
|
470
|
+
code: "canonical_cluster_id_required",
|
|
471
|
+
field: "cluster",
|
|
472
|
+
}
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
if (reference) {
|
|
476
|
+
interaction.error(
|
|
477
|
+
`${expectedKind === "cluster" ? "Cluster" : "Machine or cluster"} not found: ${reference}.`,
|
|
478
|
+
{
|
|
479
|
+
code: expectedKind === "cluster" ? "cluster_not_found" : "resource_not_found",
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (expectedKind && input.guided !== true) {
|
|
485
|
+
interaction.usage(`A ${expectedKind} reference is required unless --guided is supplied.`, {
|
|
486
|
+
code: `${expectedKind}_reference_required`,
|
|
487
|
+
field: expectedKind,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
if (!interaction.eligibleHuman || interaction.machine) {
|
|
491
|
+
interaction.usage("A machine or cluster reference is required outside an eligible terminal.", {
|
|
492
|
+
code: "resource_reference_required",
|
|
493
|
+
field: "resource",
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const resources = [];
|
|
498
|
+
if (expectedKind !== "cluster") {
|
|
499
|
+
resources.push(
|
|
500
|
+
...(await activeMachines(fetch, interaction)).map((machine) => ({ kind: "machine", machine }))
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
if (expectedKind !== "machine") {
|
|
504
|
+
resources.push(
|
|
505
|
+
...(await clusters(fetch, interaction)).map((cluster) => ({ kind: "cluster", cluster }))
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if (!resources.length)
|
|
509
|
+
interaction.error("No machine or cluster is available in this organization.", {
|
|
510
|
+
code: "resource_not_found",
|
|
511
|
+
});
|
|
512
|
+
if (resources.length === 1) return resources[0];
|
|
513
|
+
const enriched = await Promise.all(
|
|
514
|
+
resources.map(async (resource) => {
|
|
515
|
+
if (resource.kind === "machine") return resource;
|
|
516
|
+
return {
|
|
517
|
+
...resource,
|
|
518
|
+
reservation_facts: await reservationFactsForCluster(fetch, interaction, resource.cluster),
|
|
519
|
+
};
|
|
520
|
+
})
|
|
521
|
+
);
|
|
522
|
+
const choices = enriched.map(resourceChoice);
|
|
523
|
+
const selected = await interaction.ask({ label: "Resource", choices });
|
|
524
|
+
const match = enriched.find((resource) => resourceChoice(resource).value === selected);
|
|
525
|
+
if (!match)
|
|
526
|
+
interaction.usage("Choose one exact machine or cluster.", { code: "resource_ambiguous" });
|
|
527
|
+
return match;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function resourceBase(resource) {
|
|
531
|
+
const source = resource.kind === "machine" ? resource.machine : resource.cluster;
|
|
532
|
+
return {
|
|
533
|
+
resource: {
|
|
534
|
+
kind: resource.kind,
|
|
535
|
+
id: source.id,
|
|
536
|
+
reservation_id: source.reservation_id,
|
|
537
|
+
...(resource.kind === "machine"
|
|
538
|
+
? { hostname: source.hostname, access_mode: source.access_mode }
|
|
539
|
+
: { access_mode: source.access_mode, membership_count: source.machines.length }),
|
|
540
|
+
},
|
|
541
|
+
lifecycle: source.lifecycle,
|
|
542
|
+
reachability: source.reachability,
|
|
543
|
+
health: source.health,
|
|
544
|
+
metrics: null,
|
|
545
|
+
logs: null,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function sourceMachines(resource) {
|
|
550
|
+
return resource.kind === "machine"
|
|
551
|
+
? [{ id: resource.machine.id, hostname: resource.machine.hostname }]
|
|
552
|
+
: resource.cluster.machines;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function errorDetail(error) {
|
|
556
|
+
const body = record(error?.detail);
|
|
557
|
+
const candidate = body.detail ?? body.error ?? body;
|
|
558
|
+
if (typeof candidate === "string") return { code: candidate };
|
|
559
|
+
return record(candidate);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function sourceFailure(error, source, resourceRef) {
|
|
563
|
+
const status = error?.status;
|
|
564
|
+
const detail = errorDetail(error);
|
|
565
|
+
const suppliedCode = text(detail.code);
|
|
566
|
+
const explicitRetryable = typeof detail.retryable === "boolean" ? detail.retryable : null;
|
|
567
|
+
const authorization = status === 401 || status === 403;
|
|
568
|
+
const retryable = authorization
|
|
569
|
+
? false
|
|
570
|
+
: (explicitRetryable ??
|
|
571
|
+
(status === undefined || status >= 500 || [408, 425, 429].includes(status)));
|
|
572
|
+
return {
|
|
573
|
+
code: authorization
|
|
574
|
+
? status === 401
|
|
575
|
+
? "authentication_required"
|
|
576
|
+
: "authorization_denied"
|
|
577
|
+
: suppliedCode && STABLE_SOURCE_CODES.has(suppliedCode)
|
|
578
|
+
? suppliedCode
|
|
579
|
+
: status !== undefined && status < 500
|
|
580
|
+
? `${source}_rejected`
|
|
581
|
+
: `${source}_unavailable`,
|
|
582
|
+
message: authorization
|
|
583
|
+
? status === 401
|
|
584
|
+
? "The Ornn session expired while reading evidence."
|
|
585
|
+
: "The current session cannot read this evidence."
|
|
586
|
+
: `${source === "metrics" ? "Metrics" : "Logs"} are unavailable for this resource.`,
|
|
587
|
+
retryable,
|
|
588
|
+
resource_ref: resourceRef,
|
|
589
|
+
authorization,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function sleepUntil(delay, deps, signal) {
|
|
594
|
+
if (signal?.aborted) return false;
|
|
595
|
+
if (!signal) {
|
|
596
|
+
await deps.sleep(delay);
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
return new Promise((resolve, reject) => {
|
|
600
|
+
let settled = false;
|
|
601
|
+
const finish = (value) => {
|
|
602
|
+
if (settled) return;
|
|
603
|
+
settled = true;
|
|
604
|
+
signal.removeEventListener("abort", aborted);
|
|
605
|
+
resolve(value);
|
|
606
|
+
};
|
|
607
|
+
const aborted = () => finish(false);
|
|
608
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
609
|
+
Promise.resolve(deps.sleep(delay)).then(() => finish(true), reject);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function fetchEvidence(request, source, resourceRef, deps, deadline, signal) {
|
|
614
|
+
let attempt = 0;
|
|
615
|
+
for (;;) {
|
|
616
|
+
attempt += 1;
|
|
617
|
+
const remaining = deadline - nowMilliseconds(deps.now);
|
|
618
|
+
if (Number.isFinite(deadline) && remaining <= 0) {
|
|
619
|
+
return {
|
|
620
|
+
value: null,
|
|
621
|
+
error: sourceFailure(new Error("deadline elapsed"), source, resourceRef),
|
|
622
|
+
interrupted: false,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const timeoutSignal = Number.isFinite(deadline)
|
|
626
|
+
? AbortSignal.timeout(Math.max(1, Math.min(remaining, 2_147_483_647)))
|
|
627
|
+
: null;
|
|
628
|
+
const requestSignal =
|
|
629
|
+
signal && timeoutSignal
|
|
630
|
+
? AbortSignal.any([signal, timeoutSignal])
|
|
631
|
+
: (signal ?? timeoutSignal);
|
|
632
|
+
try {
|
|
633
|
+
return { value: await request(requestSignal), error: null, interrupted: false };
|
|
634
|
+
} catch (error) {
|
|
635
|
+
if (signal?.aborted) return { value: null, error: null, interrupted: true };
|
|
636
|
+
const failure = sourceFailure(error, source, resourceRef);
|
|
637
|
+
if (
|
|
638
|
+
timeoutSignal?.aborted ||
|
|
639
|
+
failure.authorization ||
|
|
640
|
+
!failure.retryable ||
|
|
641
|
+
attempt >= MAX_RETRIES
|
|
642
|
+
) {
|
|
643
|
+
return { value: null, error: failure, interrupted: false };
|
|
644
|
+
}
|
|
645
|
+
const delay = Math.min(250 * 2 ** (attempt - 1), 1_000);
|
|
646
|
+
if (
|
|
647
|
+
nowMilliseconds(deps.now) + delay >= deadline ||
|
|
648
|
+
!(await sleepUntil(delay, deps, signal))
|
|
649
|
+
) {
|
|
650
|
+
if (signal?.aborted) return { value: null, error: null, interrupted: true };
|
|
651
|
+
return { value: null, error: failure, interrupted: false };
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function safeMetricName(name) {
|
|
658
|
+
const suffix = name.match(/_(?:min|max|median|std)$/)?.[0] ?? "";
|
|
659
|
+
const base = suffix ? name.slice(0, -suffix.length) : name;
|
|
660
|
+
return SAFE_METRICS.has(base);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function safeMetricValue(value) {
|
|
664
|
+
if (typeof value === "boolean") return value;
|
|
665
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
666
|
+
if (typeof value !== "string" || !/^-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(value.trim())) {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
const numeric = Number(value);
|
|
670
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function projectMetricPoint(value) {
|
|
674
|
+
const source = record(value);
|
|
675
|
+
const observedAt = text(source.observed_at) ?? text(source.timestamp) ?? text(source.date);
|
|
676
|
+
const rawMetrics = record(source.metrics ?? source.values ?? source.resource_metrics);
|
|
677
|
+
const metrics = {};
|
|
678
|
+
for (const [name, raw] of Object.entries(rawMetrics)) {
|
|
679
|
+
const metric = safeMetricValue(raw);
|
|
680
|
+
if (safeMetricName(name) && metric !== null) metrics[name] = metric;
|
|
681
|
+
}
|
|
682
|
+
if (!observedAt || !Object.keys(metrics).length) return null;
|
|
683
|
+
return { observed_at: observedAt, metrics };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function withinEvidenceWindow(observedAt, window) {
|
|
687
|
+
const milliseconds = Date.parse(observedAt);
|
|
688
|
+
return (
|
|
689
|
+
Number.isFinite(milliseconds) && milliseconds >= window.startMs && milliseconds <= window.endMs
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function metricPoints(payload, maximum, window) {
|
|
694
|
+
const source = record(payload);
|
|
695
|
+
let raw;
|
|
696
|
+
if (Array.isArray(source.points)) {
|
|
697
|
+
raw = source.points;
|
|
698
|
+
} else if (Array.isArray(source.series)) {
|
|
699
|
+
raw = source.series.flatMap((series) =>
|
|
700
|
+
Array.isArray(record(series).points) ? series.points : []
|
|
701
|
+
);
|
|
702
|
+
} else if (source.machine && typeof source.machine === "object") {
|
|
703
|
+
raw = [
|
|
704
|
+
{
|
|
705
|
+
observed_at:
|
|
706
|
+
source.machine.observed_at ?? source.machine.resource_metrics?.last_heartbeat_at,
|
|
707
|
+
resource_metrics: source.machine.resource_metrics,
|
|
708
|
+
},
|
|
709
|
+
];
|
|
710
|
+
} else {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
return raw
|
|
714
|
+
.flatMap((point) => {
|
|
715
|
+
const projected = projectMetricPoint(point);
|
|
716
|
+
return projected && withinEvidenceWindow(projected.observed_at, window) ? [projected] : [];
|
|
717
|
+
})
|
|
718
|
+
.sort((left, right) => left.observed_at.localeCompare(right.observed_at))
|
|
719
|
+
.slice(0, maximum);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function safeLogLine(value) {
|
|
723
|
+
if (containsUnsafeFields(value)) return null;
|
|
724
|
+
const source = record(value);
|
|
725
|
+
const observedAt = text(source.date) ?? text(source.observed_at) ?? text(source.timestamp);
|
|
726
|
+
const message = text(source.message) ?? text(source.text);
|
|
727
|
+
if (!observedAt || !message || UNSAFE_LOG_MESSAGE.some((pattern) => pattern.test(message)))
|
|
728
|
+
return null;
|
|
729
|
+
const severity = stateToken(source.severity ?? "info");
|
|
730
|
+
return {
|
|
731
|
+
observed_at: observedAt,
|
|
732
|
+
severity: ["error", "warning", "info"].includes(severity) ? severity : "info",
|
|
733
|
+
message,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function logLines(payload, maximum, window) {
|
|
738
|
+
const raw = payloadRows(payload, "lines");
|
|
739
|
+
if (!raw) return null;
|
|
740
|
+
return raw
|
|
741
|
+
.flatMap((line) => {
|
|
742
|
+
const projected = safeLogLine(line);
|
|
743
|
+
return projected && withinEvidenceWindow(projected.observed_at, window) ? [projected] : [];
|
|
744
|
+
})
|
|
745
|
+
.sort(
|
|
746
|
+
(left, right) =>
|
|
747
|
+
left.observed_at.localeCompare(right.observed_at) ||
|
|
748
|
+
left.message.localeCompare(right.message)
|
|
749
|
+
)
|
|
750
|
+
.slice(0, maximum);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function invalidEvidenceResponse(code) {
|
|
754
|
+
const error = new Error("invalid evidence response");
|
|
755
|
+
error.status = 502;
|
|
756
|
+
error.detail = { code, retryable: true };
|
|
757
|
+
return error;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function query(params) {
|
|
761
|
+
return new URLSearchParams(
|
|
762
|
+
Object.entries(params).filter(([, value]) => value !== null && value !== undefined)
|
|
763
|
+
).toString();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function metricSource(machine, reservation, window, maximum, deps, deadline, signal, live) {
|
|
767
|
+
const path = live ? "live-metrics" : "telemetry-history";
|
|
768
|
+
const params = live
|
|
769
|
+
? { machine_id: machine.id }
|
|
770
|
+
: { machine_id: machine.id, start: window.start, end: window.end, max_points: String(maximum) };
|
|
771
|
+
const response = await fetchEvidence(
|
|
772
|
+
async (requestSignal) => {
|
|
773
|
+
const payload = await deps.fetch(
|
|
774
|
+
`/v1/workflows/compute/nodes/reservations/${encodeURIComponent(reservation)}/${path}?${query(params)}`,
|
|
775
|
+
{ signal: requestSignal }
|
|
776
|
+
);
|
|
777
|
+
const points = metricPoints(payload, maximum, window);
|
|
778
|
+
if (points === null) throw invalidEvidenceResponse("invalid_metrics_response");
|
|
779
|
+
return points;
|
|
780
|
+
},
|
|
781
|
+
"metrics",
|
|
782
|
+
machine.id,
|
|
783
|
+
deps,
|
|
784
|
+
deadline,
|
|
785
|
+
signal
|
|
786
|
+
);
|
|
787
|
+
if (response.interrupted) {
|
|
788
|
+
return {
|
|
789
|
+
machine_ref: machine.id,
|
|
790
|
+
state: "interrupted",
|
|
791
|
+
points: [],
|
|
792
|
+
error: null,
|
|
793
|
+
interrupted: true,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
if (response.error) {
|
|
797
|
+
return { machine_ref: machine.id, state: "unavailable", points: [], error: response.error };
|
|
798
|
+
}
|
|
799
|
+
const points = response.value;
|
|
800
|
+
return {
|
|
801
|
+
machine_ref: machine.id,
|
|
802
|
+
state: points.length ? "available" : "no_data",
|
|
803
|
+
points,
|
|
804
|
+
error: null,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function logSource(
|
|
809
|
+
machine,
|
|
810
|
+
reservation,
|
|
811
|
+
window,
|
|
812
|
+
stream,
|
|
813
|
+
limit,
|
|
814
|
+
cursor,
|
|
815
|
+
deps,
|
|
816
|
+
deadline,
|
|
817
|
+
signal
|
|
818
|
+
) {
|
|
819
|
+
const response = await fetchEvidence(
|
|
820
|
+
async (requestSignal) => {
|
|
821
|
+
const payload = await deps.fetch(
|
|
822
|
+
`/v1/workflows/compute/nodes/reservations/${encodeURIComponent(reservation)}/logs?${query({
|
|
823
|
+
machine_id: machine.id,
|
|
824
|
+
stream,
|
|
825
|
+
start: window.start,
|
|
826
|
+
end: window.end,
|
|
827
|
+
limit: String(limit),
|
|
828
|
+
cursor,
|
|
829
|
+
})}`,
|
|
830
|
+
{ signal: requestSignal }
|
|
831
|
+
);
|
|
832
|
+
const entries = logLines(payload, limit, window);
|
|
833
|
+
if (entries === null) throw invalidEvidenceResponse("invalid_logs_response");
|
|
834
|
+
return {
|
|
835
|
+
entries,
|
|
836
|
+
nextCursor: text(record(payload).next_cursor) ?? text(record(payload).nextCursor),
|
|
837
|
+
};
|
|
838
|
+
},
|
|
839
|
+
"logs",
|
|
840
|
+
machine.id,
|
|
841
|
+
deps,
|
|
842
|
+
deadline,
|
|
843
|
+
signal
|
|
844
|
+
);
|
|
845
|
+
if (response.interrupted) {
|
|
846
|
+
return {
|
|
847
|
+
machine_ref: machine.id,
|
|
848
|
+
state: "interrupted",
|
|
849
|
+
entries: [],
|
|
850
|
+
page: { limit, returned: 0, next_cursor: null },
|
|
851
|
+
error: null,
|
|
852
|
+
interrupted: true,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
if (response.error) {
|
|
856
|
+
return {
|
|
857
|
+
machine_ref: machine.id,
|
|
858
|
+
state: "unavailable",
|
|
859
|
+
entries: [],
|
|
860
|
+
page: { limit, returned: 0, next_cursor: null },
|
|
861
|
+
error: response.error,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
const { entries, nextCursor } = response.value;
|
|
865
|
+
return {
|
|
866
|
+
machine_ref: machine.id,
|
|
867
|
+
state: entries.length ? "available" : "no_data",
|
|
868
|
+
entries,
|
|
869
|
+
page: { limit, returned: entries.length, next_cursor: nextCursor },
|
|
870
|
+
error: null,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function aggregateState(sources, itemField) {
|
|
875
|
+
const unavailable = sources.filter((source) => source.state === "unavailable").length;
|
|
876
|
+
const useful = sources.some((source) => source[itemField].length > 0);
|
|
877
|
+
if (unavailable === sources.length && sources.length) return "unavailable";
|
|
878
|
+
if (unavailable) return "partial";
|
|
879
|
+
return useful ? "available" : "no_data";
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function publicMetricSource(source) {
|
|
883
|
+
return {
|
|
884
|
+
machine_ref: source.machine_ref,
|
|
885
|
+
state: source.state,
|
|
886
|
+
points: source.points,
|
|
887
|
+
...(source.error
|
|
888
|
+
? {
|
|
889
|
+
error: {
|
|
890
|
+
code: source.error.code,
|
|
891
|
+
message: source.error.message,
|
|
892
|
+
retryable: source.error.retryable,
|
|
893
|
+
},
|
|
894
|
+
}
|
|
895
|
+
: {}),
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function publicLogSource(source) {
|
|
900
|
+
return {
|
|
901
|
+
machine_ref: source.machine_ref,
|
|
902
|
+
state: source.state,
|
|
903
|
+
entries: source.entries,
|
|
904
|
+
page: source.page,
|
|
905
|
+
...(source.error
|
|
906
|
+
? {
|
|
907
|
+
error: {
|
|
908
|
+
code: source.error.code,
|
|
909
|
+
message: source.error.message,
|
|
910
|
+
retryable: source.error.retryable,
|
|
911
|
+
},
|
|
912
|
+
}
|
|
913
|
+
: {}),
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async function collectEvidence(
|
|
918
|
+
resource,
|
|
919
|
+
requested,
|
|
920
|
+
window,
|
|
921
|
+
options,
|
|
922
|
+
deps,
|
|
923
|
+
signal,
|
|
924
|
+
{ live = false } = {}
|
|
925
|
+
) {
|
|
926
|
+
const machines = sourceMachines(resource);
|
|
927
|
+
const reservation =
|
|
928
|
+
resource.kind === "machine" ? resource.machine.reservation_id : resource.cluster.reservation_id;
|
|
929
|
+
const deadline = options.deadline;
|
|
930
|
+
const errors = [];
|
|
931
|
+
let metrics = null;
|
|
932
|
+
let logs = null;
|
|
933
|
+
let interrupted = false;
|
|
934
|
+
|
|
935
|
+
if (requested.metrics) {
|
|
936
|
+
const sources = [];
|
|
937
|
+
for (const machine of machines) {
|
|
938
|
+
const source = await metricSource(
|
|
939
|
+
machine,
|
|
940
|
+
reservation,
|
|
941
|
+
window,
|
|
942
|
+
options.maxPoints,
|
|
943
|
+
deps,
|
|
944
|
+
deadline,
|
|
945
|
+
signal,
|
|
946
|
+
live
|
|
947
|
+
);
|
|
948
|
+
sources.push(source);
|
|
949
|
+
if (source.interrupted) {
|
|
950
|
+
interrupted = true;
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
if (source.error) errors.push(source.error);
|
|
954
|
+
if (source.error?.authorization) break;
|
|
955
|
+
}
|
|
956
|
+
metrics = {
|
|
957
|
+
state: aggregateState(sources, "points"),
|
|
958
|
+
window: { start: window.start, end: window.end, max_points: options.maxPoints },
|
|
959
|
+
series: sources.map(publicMetricSource),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (requested.logs && !interrupted && !errors.some((error) => error.authorization)) {
|
|
964
|
+
const sources = [];
|
|
965
|
+
for (const machine of machines) {
|
|
966
|
+
const source = await logSource(
|
|
967
|
+
machine,
|
|
968
|
+
reservation,
|
|
969
|
+
window,
|
|
970
|
+
options.stream,
|
|
971
|
+
options.limit,
|
|
972
|
+
options.cursor,
|
|
973
|
+
deps,
|
|
974
|
+
deadline,
|
|
975
|
+
signal
|
|
976
|
+
);
|
|
977
|
+
sources.push(source);
|
|
978
|
+
if (source.interrupted) {
|
|
979
|
+
interrupted = true;
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
if (source.error) errors.push(source.error);
|
|
983
|
+
if (source.error?.authorization) break;
|
|
984
|
+
}
|
|
985
|
+
logs = {
|
|
986
|
+
state: aggregateState(sources, "entries"),
|
|
987
|
+
stream: options.stream,
|
|
988
|
+
window: { start: window.start, end: window.end },
|
|
989
|
+
sources: sources.map(publicLogSource),
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return { errors, interrupted, logs, metrics };
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function publicErrors(errors) {
|
|
997
|
+
return errors.map(({ authorization: _authorization, ...error }) => error);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function snapshotExit(errors, requestedCount) {
|
|
1001
|
+
if (!errors.length) return { exitCode: 0, outcome: null };
|
|
1002
|
+
if (errors.some((error) => error.authorization)) {
|
|
1003
|
+
return {
|
|
1004
|
+
exitCode: 5,
|
|
1005
|
+
outcome: { class: "failed", phase: errors.find((error) => error.authorization).code },
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
if (errors.length < requestedCount) {
|
|
1009
|
+
return { exitCode: 4, outcome: { class: "partial", phase: "evidence_partial" } };
|
|
1010
|
+
}
|
|
1011
|
+
if (errors.every((error) => error.retryable)) {
|
|
1012
|
+
return { exitCode: 6, outcome: { class: "failed", phase: "evidence_unavailable" } };
|
|
1013
|
+
}
|
|
1014
|
+
return { exitCode: 1, outcome: { class: "failed", phase: "evidence_failed" } };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function evidenceSourceLabel(data, index) {
|
|
1018
|
+
if (data.resource.kind === "machine") return data.resource.hostname ?? "Machine";
|
|
1019
|
+
return `Cluster member ${index + 1}`;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function metricSummary(metrics) {
|
|
1023
|
+
return Object.entries(metrics)
|
|
1024
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1025
|
+
.map(([name, value]) => `${name.replaceAll("_", " ")}=${value}`)
|
|
1026
|
+
.join(", ");
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function evidenceLines(data) {
|
|
1030
|
+
const lines = [
|
|
1031
|
+
data.resource.kind === "machine"
|
|
1032
|
+
? `Machine: ${data.resource.hostname ?? "selected machine"}`
|
|
1033
|
+
: `${data.resource.access_mode} cluster`,
|
|
1034
|
+
`Lifecycle: ${data.lifecycle.class} (${data.lifecycle.phase})`,
|
|
1035
|
+
`Reachability: ${data.reachability.state}`,
|
|
1036
|
+
`Health: ${data.health.state}`,
|
|
1037
|
+
];
|
|
1038
|
+
if (data.metrics) {
|
|
1039
|
+
lines.push(`Metrics: ${data.metrics.state}`);
|
|
1040
|
+
for (const [index, source] of data.metrics.series.entries()) {
|
|
1041
|
+
const point = source.points.at(-1);
|
|
1042
|
+
if (point) {
|
|
1043
|
+
lines.push(
|
|
1044
|
+
` ${evidenceSourceLabel(data, index)} · ${point.observed_at} · ${metricSummary(point.metrics)}`
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
if (data.logs) {
|
|
1050
|
+
lines.push(`Logs (${data.logs.stream}): ${data.logs.state}`);
|
|
1051
|
+
for (const [index, source] of data.logs.sources.entries()) {
|
|
1052
|
+
for (const entry of source.entries) {
|
|
1053
|
+
lines.push(
|
|
1054
|
+
` ${evidenceSourceLabel(data, index)} · ${entry.observed_at} · ${entry.severity} · ${entry.message}`
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return lines;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function pageFor(resource, requested, logs) {
|
|
1063
|
+
return resource.kind === "machine" && requested.logs && !requested.metrics
|
|
1064
|
+
? (logs?.sources?.[0]?.page ?? null)
|
|
1065
|
+
: null;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
async function snapshot(resource, requested, window, options, deps, interaction) {
|
|
1069
|
+
const collected = await collectEvidence(resource, requested, window, options, deps, null);
|
|
1070
|
+
const data = { ...resourceBase(resource), metrics: collected.metrics, logs: collected.logs };
|
|
1071
|
+
const requestedCount =
|
|
1072
|
+
sourceMachines(resource).length * (Number(requested.metrics) + Number(requested.logs));
|
|
1073
|
+
const status = snapshotExit(collected.errors, requestedCount);
|
|
1074
|
+
return interaction.result({
|
|
1075
|
+
data,
|
|
1076
|
+
errors: publicErrors(collected.errors),
|
|
1077
|
+
exitCode: status.exitCode,
|
|
1078
|
+
outcome: status.outcome ?? data.lifecycle,
|
|
1079
|
+
page: pageFor(resource, requested, data.logs),
|
|
1080
|
+
presentation: { lines: evidenceLines(data) },
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function streamState(data, state) {
|
|
1085
|
+
return {
|
|
1086
|
+
...data,
|
|
1087
|
+
metrics: data.metrics ? { ...data.metrics, state } : null,
|
|
1088
|
+
logs: data.logs ? { ...data.logs, state } : null,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function liveState(data) {
|
|
1093
|
+
const metricState = data.metrics?.state;
|
|
1094
|
+
const logState = data.logs?.state;
|
|
1095
|
+
return {
|
|
1096
|
+
...data,
|
|
1097
|
+
metrics: data.metrics
|
|
1098
|
+
? {
|
|
1099
|
+
...data.metrics,
|
|
1100
|
+
state: ["partial", "unavailable"].includes(metricState)
|
|
1101
|
+
? metricState
|
|
1102
|
+
: data.metrics.series.some((source) => source.points.length)
|
|
1103
|
+
? "streaming"
|
|
1104
|
+
: "waiting_for_data",
|
|
1105
|
+
}
|
|
1106
|
+
: null,
|
|
1107
|
+
logs: data.logs
|
|
1108
|
+
? {
|
|
1109
|
+
...data.logs,
|
|
1110
|
+
state: ["partial", "unavailable"].includes(logState)
|
|
1111
|
+
? logState
|
|
1112
|
+
: data.logs.sources.some((source) => source.entries.length)
|
|
1113
|
+
? "streaming"
|
|
1114
|
+
: "waiting_for_data",
|
|
1115
|
+
}
|
|
1116
|
+
: null,
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function writeHumanProgress(stdout, data) {
|
|
1121
|
+
if (data.metrics) {
|
|
1122
|
+
for (const [index, source] of data.metrics.series.entries()) {
|
|
1123
|
+
const point = source.points.at(-1);
|
|
1124
|
+
if (point) {
|
|
1125
|
+
stdout.write(
|
|
1126
|
+
`Metric ${evidenceSourceLabel(data, index)} ${point.observed_at} ${metricSummary(point.metrics)}\n`
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
if (data.logs) {
|
|
1132
|
+
for (const [index, source] of data.logs.sources.entries()) {
|
|
1133
|
+
for (const line of source.entries) {
|
|
1134
|
+
stdout.write(
|
|
1135
|
+
`${evidenceSourceLabel(data, index)} ${line.observed_at} ${line.severity} ${line.message}\n`
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function streamResult(interaction, data, progress, exitCode, outcome, errors = []) {
|
|
1143
|
+
return interaction.result({
|
|
1144
|
+
data,
|
|
1145
|
+
errors: publicErrors(errors),
|
|
1146
|
+
exitCode,
|
|
1147
|
+
outcome,
|
|
1148
|
+
progress,
|
|
1149
|
+
presentation: { lines: evidenceLines(data) },
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async function follow(resource, requested, initialWindow, options, deps, interaction) {
|
|
1154
|
+
if (interaction.format === "json") {
|
|
1155
|
+
interaction.usage("JSON cannot represent a follow stream. Use --format ndjson.", {
|
|
1156
|
+
code: "json_follow_unsupported",
|
|
1157
|
+
field: "format",
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
const count = integerOption(interaction, interaction.options.count, "count", null, MAX_PAGE_SIZE);
|
|
1161
|
+
const timeoutMs = durationMilliseconds(interaction, interaction.options.timeout, "timeout");
|
|
1162
|
+
const intervalMs =
|
|
1163
|
+
durationMilliseconds(interaction, interaction.options.interval, "interval") ??
|
|
1164
|
+
DEFAULT_FOLLOW_INTERVAL_MS;
|
|
1165
|
+
if (interaction.machine && count === null && timeoutMs === null) {
|
|
1166
|
+
interaction.usage("Machine follow requires --count or --timeout so the stream is bounded.", {
|
|
1167
|
+
code: "follow_bound_required",
|
|
1168
|
+
field: "count",
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const localController = deps.signal ? null : new AbortController();
|
|
1173
|
+
const signal = deps.signal ?? localController.signal;
|
|
1174
|
+
const onSigint = localController ? () => localController.abort() : null;
|
|
1175
|
+
if (onSigint) process.once("SIGINT", onSigint);
|
|
1176
|
+
const startedAt = nowMilliseconds(deps.now);
|
|
1177
|
+
const deadline = timeoutMs === null ? Number.POSITIVE_INFINITY : startedAt + timeoutMs;
|
|
1178
|
+
let current = resourceBase(resource);
|
|
1179
|
+
const progress = [];
|
|
1180
|
+
let iteration = 0;
|
|
1181
|
+
try {
|
|
1182
|
+
for (;;) {
|
|
1183
|
+
if (signal.aborted) {
|
|
1184
|
+
current = streamState(current, "interrupted");
|
|
1185
|
+
return streamResult(interaction, current, progress, 130, {
|
|
1186
|
+
class: "ended",
|
|
1187
|
+
phase: "interrupted",
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
const observedAt = nowMilliseconds(deps.now);
|
|
1191
|
+
if (observedAt >= deadline) {
|
|
1192
|
+
current = streamState(current, "ended");
|
|
1193
|
+
return streamResult(interaction, current, progress, 124, {
|
|
1194
|
+
class: "ended",
|
|
1195
|
+
phase: "observation_timeout",
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
iteration += 1;
|
|
1199
|
+
const window = {
|
|
1200
|
+
start: initialWindow.start,
|
|
1201
|
+
end: new Date(observedAt).toISOString(),
|
|
1202
|
+
startMs: initialWindow.startMs,
|
|
1203
|
+
endMs: observedAt,
|
|
1204
|
+
};
|
|
1205
|
+
const collected = await collectEvidence(
|
|
1206
|
+
resource,
|
|
1207
|
+
requested,
|
|
1208
|
+
window,
|
|
1209
|
+
{ ...options, deadline },
|
|
1210
|
+
deps,
|
|
1211
|
+
signal,
|
|
1212
|
+
{ live: true }
|
|
1213
|
+
);
|
|
1214
|
+
if (collected.interrupted || signal.aborted) {
|
|
1215
|
+
current = streamState(
|
|
1216
|
+
{ ...resourceBase(resource), metrics: collected.metrics, logs: collected.logs },
|
|
1217
|
+
"interrupted"
|
|
1218
|
+
);
|
|
1219
|
+
return streamResult(interaction, current, progress, 130, {
|
|
1220
|
+
class: "ended",
|
|
1221
|
+
phase: "interrupted",
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
current = liveState({
|
|
1225
|
+
...resourceBase(resource),
|
|
1226
|
+
metrics: collected.metrics,
|
|
1227
|
+
logs: collected.logs,
|
|
1228
|
+
});
|
|
1229
|
+
if (collected.errors.length) {
|
|
1230
|
+
const requestedCount =
|
|
1231
|
+
sourceMachines(resource).length * (Number(requested.metrics) + Number(requested.logs));
|
|
1232
|
+
const status = snapshotExit(collected.errors, requestedCount);
|
|
1233
|
+
return streamResult(
|
|
1234
|
+
interaction,
|
|
1235
|
+
current,
|
|
1236
|
+
progress,
|
|
1237
|
+
status.exitCode,
|
|
1238
|
+
status.outcome,
|
|
1239
|
+
collected.errors
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
const progressEntry = {
|
|
1243
|
+
data: current,
|
|
1244
|
+
message: `Observed ${requested.metrics && requested.logs ? "metrics and logs" : requested.metrics ? "metrics" : "logs"}.`,
|
|
1245
|
+
observedAt: new Date(observedAt).toISOString(),
|
|
1246
|
+
outcome: { class: "working", phase: "streaming" },
|
|
1247
|
+
sequence: iteration,
|
|
1248
|
+
};
|
|
1249
|
+
if (interaction.machine) progress.push(progressEntry);
|
|
1250
|
+
else writeHumanProgress(deps.stdout, current);
|
|
1251
|
+
|
|
1252
|
+
if (count !== null && iteration >= count) {
|
|
1253
|
+
current = streamState(current, "ended");
|
|
1254
|
+
return streamResult(interaction, current, progress, 0, {
|
|
1255
|
+
class: "ended",
|
|
1256
|
+
phase: "observation_complete",
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
const remaining = deadline - nowMilliseconds(deps.now);
|
|
1260
|
+
if (remaining <= 0) continue;
|
|
1261
|
+
const continued = await sleepUntil(Math.min(intervalMs, remaining), deps, signal);
|
|
1262
|
+
if (!continued) {
|
|
1263
|
+
current = streamState(current, "interrupted");
|
|
1264
|
+
return streamResult(interaction, current, progress, 130, {
|
|
1265
|
+
class: "ended",
|
|
1266
|
+
phase: "interrupted",
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
} finally {
|
|
1271
|
+
if (onSigint) process.removeListener("SIGINT", onSigint);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
export async function executeEvidenceCommand(command, input, deps) {
|
|
1276
|
+
const interaction = deps.interaction;
|
|
1277
|
+
if (input.follow === true && interaction.format === "json") {
|
|
1278
|
+
interaction.usage("JSON cannot represent a follow stream. Use --format ndjson.", {
|
|
1279
|
+
code: "json_follow_unsupported",
|
|
1280
|
+
field: "format",
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
const requested = {
|
|
1284
|
+
metrics: command === "check" || command.endsWith(".metrics"),
|
|
1285
|
+
logs: command === "check" || command.endsWith(".logs"),
|
|
1286
|
+
};
|
|
1287
|
+
const logsOnly = requested.logs && !requested.metrics;
|
|
1288
|
+
const window = evidenceWindow(input, interaction, deps.now, {
|
|
1289
|
+
follow: input.follow === true,
|
|
1290
|
+
logsOnly,
|
|
1291
|
+
});
|
|
1292
|
+
const maxPoints = integerOption(interaction, input.maxPoints, "max-points", DEFAULT_MAX_POINTS);
|
|
1293
|
+
const limit = integerOption(interaction, input.limit, "limit", DEFAULT_LOG_LIMIT);
|
|
1294
|
+
const stream = input.stream ?? "kernel";
|
|
1295
|
+
if (
|
|
1296
|
+
!requested.logs &&
|
|
1297
|
+
(input.stream !== undefined ||
|
|
1298
|
+
input.limit !== undefined ||
|
|
1299
|
+
input.cursor !== undefined ||
|
|
1300
|
+
input.before !== undefined)
|
|
1301
|
+
) {
|
|
1302
|
+
interaction.usage("Log options are not accepted by a metrics-only command.", {
|
|
1303
|
+
code: "invalid_input",
|
|
1304
|
+
field: "stream",
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
if (!requested.metrics && input.maxPoints !== undefined) {
|
|
1308
|
+
interaction.usage("--max-points is not accepted by a logs-only command.", {
|
|
1309
|
+
code: "invalid_input",
|
|
1310
|
+
field: "maxPoints",
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
if (
|
|
1314
|
+
input.follow !== true &&
|
|
1315
|
+
(input.count !== undefined || input.timeout !== undefined || input.interval !== undefined)
|
|
1316
|
+
) {
|
|
1317
|
+
interaction.usage("--count, --timeout, and --interval require --follow.", {
|
|
1318
|
+
code: "invalid_input",
|
|
1319
|
+
field:
|
|
1320
|
+
input.count !== undefined ? "count" : input.timeout !== undefined ? "timeout" : "interval",
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
if (input.cursor !== undefined && input.follow === true) {
|
|
1324
|
+
interaction.usage("--cursor cannot be combined with --follow.", {
|
|
1325
|
+
code: "invalid_input",
|
|
1326
|
+
field: "cursor",
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const resource = await resolveResource(command, input, deps.fetch, interaction);
|
|
1331
|
+
const options = {
|
|
1332
|
+
cursor: text(input.cursor),
|
|
1333
|
+
deadline: nowMilliseconds(deps.now) + DEFAULT_RETRY_WINDOW_MS,
|
|
1334
|
+
limit,
|
|
1335
|
+
maxPoints,
|
|
1336
|
+
stream,
|
|
1337
|
+
};
|
|
1338
|
+
if (input.follow === true) return follow(resource, requested, window, options, deps, interaction);
|
|
1339
|
+
return snapshot(resource, requested, window, options, deps, interaction);
|
|
1340
|
+
}
|