@ornncompute/cli 1.0.72 → 1.0.75
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 +24 -20
- package/package.json +1 -1
- package/src/admin-capacity-reservations.mjs +2 -12
- package/src/admin-command-handlers.mjs +0 -1
- package/src/admin-credits.mjs +1 -2
- package/src/admin-enrollment.mjs +22 -30
- package/src/admin-mutations.mjs +6 -56
- package/src/admin-node-diagnostics.mjs +2 -36
- package/src/admin-node-lifecycle.mjs +25 -43
- package/src/admin-shell.mjs +39 -32
- package/src/external-bucket-commands.mjs +5 -73
- package/src/interaction-runtime.mjs +2 -37
- package/src/kubernetes-commands.mjs +9 -100
- package/src/launch-mode-handlers.mjs +0 -1
- package/src/mount-commands.mjs +25 -131
- package/src/storage-commands.mjs +2 -87
- package/src/vm-commands.mjs +9 -42
- package/vendor/capabilities/capabilities/admin-enrollment.js +18 -17
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +3 -101
- package/vendor/capabilities/capabilities/admin.js +3 -3
- package/vendor/capabilities/capabilities/clusters.js +3 -3
- package/vendor/capabilities/capabilities/commerce-market.js +9 -9
- package/vendor/capabilities/capabilities/compute.js +7 -7
- package/vendor/capabilities/capabilities/credits.js +7 -2
- package/vendor/capabilities/capabilities/external-buckets.js +4 -4
- package/vendor/capabilities/capabilities/fleet.js +109 -29
- package/vendor/capabilities/capabilities/market-shared.js +1 -1
- package/vendor/capabilities/capabilities/markets.js +12 -12
- package/vendor/capabilities/capabilities/mounts.js +4 -4
- package/vendor/capabilities/capabilities/networks.js +5 -5
- package/vendor/capabilities/capabilities/observability.js +49 -14
- package/vendor/capabilities/capabilities/tokens.js +5 -9
- package/vendor/capabilities/capabilities/users.js +8 -10
- package/vendor/capabilities/capabilities/volumes.js +5 -5
|
@@ -9,9 +9,9 @@ const POLL_INTERVAL_MS = 1_000;
|
|
|
9
9
|
const COMMAND_BY_CAPABILITY = Object.freeze({
|
|
10
10
|
"nodes.reboot": "reboot",
|
|
11
11
|
"nodes.hard_reset": "hard-reset",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
12
|
+
"node.off-grid": "suspend-management",
|
|
13
|
+
"node.on-grid": "resume-management",
|
|
14
|
+
"node.terminate": "terminate",
|
|
15
15
|
"nodes.assign": "assign",
|
|
16
16
|
});
|
|
17
17
|
|
|
@@ -22,17 +22,17 @@ const CAPABILITY_BY_COMMAND = Object.freeze(
|
|
|
22
22
|
);
|
|
23
23
|
|
|
24
24
|
const USAGE = Object.freeze({
|
|
25
|
-
reboot: "Usage: ornn admin nodes reboot <node-ref> [--
|
|
25
|
+
reboot: "Usage: ornn admin nodes reboot <node-ref> [--no-wait] [--timeout SECONDS]",
|
|
26
26
|
"hard-reset":
|
|
27
|
-
"Usage: ornn admin nodes hard-reset <node-ref> --erase-local-data [--
|
|
27
|
+
"Usage: ornn admin nodes hard-reset <node-ref> --erase-local-data [--no-wait] [--timeout SECONDS]",
|
|
28
28
|
"suspend-management":
|
|
29
|
-
"Usage: ornn admin nodes suspend-management <node-ref> [--reason TEXT] [--
|
|
29
|
+
"Usage: ornn admin nodes suspend-management <node-ref> [--reason TEXT] [--idempotency-key KEY] [--wait|--no-wait] [--timeout SECONDS]",
|
|
30
30
|
"resume-management":
|
|
31
|
-
"Usage: ornn admin nodes resume-management <node-ref> [--
|
|
31
|
+
"Usage: ornn admin nodes resume-management <node-ref> [--idempotency-key KEY] [--wait|--no-wait] [--timeout SECONDS]",
|
|
32
32
|
terminate:
|
|
33
|
-
"Usage: ornn admin nodes terminate <node-ref> --delete-node-record [--skip-cleanup] [--reason TEXT] [--
|
|
33
|
+
"Usage: ornn admin nodes terminate <node-ref> --delete-node-record [--skip-cleanup] [--reason TEXT] [--idempotency-key KEY]",
|
|
34
34
|
assign:
|
|
35
|
-
"Usage: ornn admin nodes assign <node-ref> --reservation RESERVATION [--
|
|
35
|
+
"Usage: ornn admin nodes assign <node-ref> --reservation RESERVATION [--idempotency-key KEY] [--wait|--no-wait] [--timeout SECONDS]",
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
const BOOLEAN_OPTIONS = new Set([
|
|
@@ -41,16 +41,15 @@ const BOOLEAN_OPTIONS = new Set([
|
|
|
41
41
|
"no-wait",
|
|
42
42
|
"skip-cleanup",
|
|
43
43
|
"wait",
|
|
44
|
-
"yes",
|
|
45
44
|
]);
|
|
46
45
|
const VALUE_OPTIONS = new Set(["idempotency-key", "reason", "reservation", "timeout"]);
|
|
47
46
|
const ALLOWED_OPTIONS = Object.freeze({
|
|
48
|
-
reboot: new Set(["
|
|
49
|
-
"hard-reset": new Set(["erase-local-data", "
|
|
50
|
-
"suspend-management": new Set(["reason", "
|
|
51
|
-
"resume-management": new Set(["
|
|
52
|
-
terminate: new Set(["delete-node-record", "skip-cleanup", "reason", "
|
|
53
|
-
assign: new Set(["reservation", "
|
|
47
|
+
reboot: new Set(["wait", "no-wait", "timeout"]),
|
|
48
|
+
"hard-reset": new Set(["erase-local-data", "wait", "no-wait", "timeout"]),
|
|
49
|
+
"suspend-management": new Set(["reason", "idempotency-key", "wait", "no-wait", "timeout"]),
|
|
50
|
+
"resume-management": new Set(["idempotency-key", "wait", "no-wait", "timeout"]),
|
|
51
|
+
terminate: new Set(["delete-node-record", "skip-cleanup", "reason", "idempotency-key"]),
|
|
52
|
+
assign: new Set(["reservation", "idempotency-key", "wait", "no-wait", "timeout"]),
|
|
54
53
|
});
|
|
55
54
|
|
|
56
55
|
function mutationError(message, { code = "usage_error", field } = {}) {
|
|
@@ -135,22 +134,16 @@ export function parseAdminNodeLifecycleInput(command, argv) {
|
|
|
135
134
|
});
|
|
136
135
|
}
|
|
137
136
|
if (name === "hard-reset" && options["erase-local-data"] !== true) {
|
|
138
|
-
throw mutationError(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
field: "erase-local-data",
|
|
143
|
-
}
|
|
144
|
-
);
|
|
137
|
+
throw mutationError(`${usage}\nHard reset requires --erase-local-data.`, {
|
|
138
|
+
code: "erase_local_data_required",
|
|
139
|
+
field: "erase-local-data",
|
|
140
|
+
});
|
|
145
141
|
}
|
|
146
142
|
if (name === "terminate" && options["delete-node-record"] !== true) {
|
|
147
|
-
throw mutationError(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
field: "delete-node-record",
|
|
152
|
-
}
|
|
153
|
-
);
|
|
143
|
+
throw mutationError(`${usage}\nTermination requires --delete-node-record.`, {
|
|
144
|
+
code: "delete_node_record_required",
|
|
145
|
+
field: "delete-node-record",
|
|
146
|
+
});
|
|
154
147
|
}
|
|
155
148
|
if (name === "assign" && !options.reservation) {
|
|
156
149
|
throw mutationError(`${usage}\nAssignment requires --reservation.`, {
|
|
@@ -170,12 +163,11 @@ export function parseAdminNodeLifecycleInput(command, argv) {
|
|
|
170
163
|
const idempotencyKey = options["idempotency-key"]?.trim();
|
|
171
164
|
const safety =
|
|
172
165
|
name === "reboot" || name === "hard-reset"
|
|
173
|
-
? ADMIN_MUTATION_SAFETY.
|
|
166
|
+
? ADMIN_MUTATION_SAFETY.direct
|
|
174
167
|
: ADMIN_MUTATION_SAFETY.reconcile;
|
|
175
168
|
return {
|
|
176
169
|
input: {
|
|
177
170
|
node_ref: nodeRef,
|
|
178
|
-
...(options.yes ? { yes: true } : {}),
|
|
179
171
|
...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
|
|
180
172
|
...(options.reason ? { reason: options.reason } : {}),
|
|
181
173
|
...(options.wait ? { wait: true } : options["no-wait"] ? { wait: false } : {}),
|
|
@@ -192,7 +184,6 @@ export function parseAdminNodeLifecycleInput(command, argv) {
|
|
|
192
184
|
mutation: {
|
|
193
185
|
idempotencyKey,
|
|
194
186
|
safety,
|
|
195
|
-
yes: options.yes === true,
|
|
196
187
|
},
|
|
197
188
|
};
|
|
198
189
|
}
|
|
@@ -900,15 +891,6 @@ export async function runAdminNodeLifecycle({
|
|
|
900
891
|
},
|
|
901
892
|
});
|
|
902
893
|
|
|
903
|
-
if (result.status === "declined") {
|
|
904
|
-
return {
|
|
905
|
-
...result,
|
|
906
|
-
action: null,
|
|
907
|
-
resume: null,
|
|
908
|
-
errors: [],
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
|
|
912
894
|
const accepted = {
|
|
913
895
|
operation: command,
|
|
914
896
|
reconciled: result.reconciled === true || lateReconciled,
|
|
@@ -939,7 +921,7 @@ export const adminNodeLifecycleHandlers = Object.keys(COMMAND_BY_CAPABILITY).map
|
|
|
939
921
|
safety:
|
|
940
922
|
COMMAND_BY_CAPABILITY[capabilityId] === "reboot" ||
|
|
941
923
|
COMMAND_BY_CAPABILITY[capabilityId] === "hard-reset"
|
|
942
|
-
? ADMIN_MUTATION_SAFETY.
|
|
924
|
+
? ADMIN_MUTATION_SAFETY.direct
|
|
943
925
|
: ADMIN_MUTATION_SAFETY.reconcile,
|
|
944
926
|
run: runAdminNodeLifecycle,
|
|
945
927
|
})
|
package/src/admin-shell.mjs
CHANGED
|
@@ -185,13 +185,21 @@ function parseAdminInput(capability, argv) {
|
|
|
185
185
|
throw new AdminUsageError(usage);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
const offsetPagination = ["users.list", "organization.list"].includes(
|
|
188
|
+
const offsetPagination = ["users.list", "organization.list", "supplier.list"].includes(
|
|
189
|
+
capability.id
|
|
190
|
+
);
|
|
191
|
+
const directoryPagination = ["users.list", "organization.list"].includes(capability.id);
|
|
189
192
|
const reservationList = capability.id === "reservation.list";
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
"
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
const maxLimit = directoryPagination
|
|
194
|
+
? 100
|
|
195
|
+
: capability.id === "supplier.list"
|
|
196
|
+
? 1000
|
|
197
|
+
: reservationList
|
|
198
|
+
? 100
|
|
199
|
+
: MAX_PAGE_LIMIT;
|
|
200
|
+
const limit =
|
|
201
|
+
positiveInteger(options.limit, "limit", maxLimit) ??
|
|
202
|
+
(directoryPagination ? 25 : DEFAULT_PAGE_LIMIT);
|
|
195
203
|
const input = {};
|
|
196
204
|
if (nodeCommand) input.node_id = positionals[0];
|
|
197
205
|
if (options.supplier) input.organization_id = options.supplier;
|
|
@@ -232,13 +240,8 @@ function parseAdminInput(capability, argv) {
|
|
|
232
240
|
if (options.cursor) input.cursor = options.cursor;
|
|
233
241
|
}
|
|
234
242
|
if (offsetPagination) {
|
|
235
|
-
if (explicitOffset !== undefined && options.cursor) {
|
|
236
|
-
throw new AdminUsageError("--offset and --cursor cannot be used together.", {
|
|
237
|
-
field: "offset",
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
243
|
input.limit = limit;
|
|
241
|
-
input.offset = explicitOffset ??
|
|
244
|
+
input.offset = explicitOffset ?? 0;
|
|
242
245
|
}
|
|
243
246
|
|
|
244
247
|
return {
|
|
@@ -328,14 +331,9 @@ function collectionKey(capability) {
|
|
|
328
331
|
if (root === "nodes" && leaf === "logs") return "logs";
|
|
329
332
|
if (!["list", "search"].includes(leaf)) return null;
|
|
330
333
|
if (root === "enrollment-tokens") return "enrollment_tokens";
|
|
331
|
-
return [
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
"organizations",
|
|
335
|
-
"reservations",
|
|
336
|
-
"suppliers",
|
|
337
|
-
"users",
|
|
338
|
-
].includes(root)
|
|
334
|
+
return ["facilities", "nodes", "organizations", "reservations", "suppliers", "users"].includes(
|
|
335
|
+
root
|
|
336
|
+
)
|
|
339
337
|
? root
|
|
340
338
|
: null;
|
|
341
339
|
}
|
|
@@ -445,14 +443,19 @@ export function finalizeAdminResult(capability, payload, pagination) {
|
|
|
445
443
|
pagination.ownerOffsetPagination && payload && !Array.isArray(payload)
|
|
446
444
|
? payload.next_offset
|
|
447
445
|
: undefined;
|
|
446
|
+
const nextOffset = pagination.ownerOffsetPagination
|
|
447
|
+
? Number.isSafeInteger(ownerNextOffset) && ownerNextOffset > offset
|
|
448
|
+
? ownerNextOffset
|
|
449
|
+
: selected.length === pagination.limit
|
|
450
|
+
? offset + selected.length
|
|
451
|
+
: null
|
|
452
|
+
: undefined;
|
|
448
453
|
const ownerNextCursor =
|
|
449
454
|
payload && !Array.isArray(payload)
|
|
450
455
|
? (payload.next_cursor ?? payload.nextCursor ?? payload.page?.next_cursor ?? null)
|
|
451
456
|
: null;
|
|
452
457
|
let nextCursor = pagination.ownerOffsetPagination
|
|
453
|
-
?
|
|
454
|
-
? encodeCursor(command, ownerNextOffset)
|
|
455
|
-
: null
|
|
458
|
+
? null
|
|
456
459
|
: !pagination.ownerPagination && offset + selected.length < rows.length
|
|
457
460
|
? encodeCursor(command, offset + selected.length)
|
|
458
461
|
: (ownerNextCursor ?? null);
|
|
@@ -480,11 +483,9 @@ export function finalizeAdminResult(capability, payload, pagination) {
|
|
|
480
483
|
}
|
|
481
484
|
return {
|
|
482
485
|
data: { [key]: selected },
|
|
483
|
-
page:
|
|
484
|
-
limit: pagination.limit,
|
|
485
|
-
returned: selected.length,
|
|
486
|
-
next_cursor: nextCursor,
|
|
487
|
-
},
|
|
486
|
+
page: pagination.ownerOffsetPagination
|
|
487
|
+
? { limit: pagination.limit, returned: selected.length, next_offset: nextOffset }
|
|
488
|
+
: { limit: pagination.limit, returned: selected.length, next_cursor: nextCursor },
|
|
488
489
|
};
|
|
489
490
|
}
|
|
490
491
|
|
|
@@ -515,7 +516,15 @@ export function writeAdminResultHuman(stdout, command, result, input = {}) {
|
|
|
515
516
|
} else {
|
|
516
517
|
for (const row of rows) stdout.write(`${JSON.stringify(row)}\n`);
|
|
517
518
|
}
|
|
518
|
-
|
|
519
|
+
const nextPage =
|
|
520
|
+
result.page.next_offset !== undefined
|
|
521
|
+
? result.page.next_offset === null
|
|
522
|
+
? null
|
|
523
|
+
: `--offset ${result.page.next_offset}`
|
|
524
|
+
: result.page.next_cursor
|
|
525
|
+
? `--cursor ${shellSingleQuote(result.page.next_cursor)}`
|
|
526
|
+
: null;
|
|
527
|
+
if (nextPage) {
|
|
519
528
|
const target = command === "admin nodes logs" ? ` ${shellSingleQuote(input.node_id)}` : "";
|
|
520
529
|
const filters = [
|
|
521
530
|
["supplier", command === "admin users list" ? undefined : input.organization_id],
|
|
@@ -535,8 +544,6 @@ export function writeAdminResultHuman(stdout, command, result, input = {}) {
|
|
|
535
544
|
.filter(([, value]) => value !== undefined)
|
|
536
545
|
.map(([name, value]) => ` --${name} ${shellSingleQuote(String(value))}`)
|
|
537
546
|
.join("");
|
|
538
|
-
stdout.write(
|
|
539
|
-
`Next: ornn ${command}${target} --cursor ${shellSingleQuote(result.page.next_cursor)}${filters}\n`
|
|
540
|
-
);
|
|
547
|
+
stdout.write(`Next: ornn ${command}${target} ${nextPage}${filters}\n`);
|
|
541
548
|
}
|
|
542
549
|
}
|
|
@@ -402,7 +402,7 @@ function connectionEquivalent(drive, intent) {
|
|
|
402
402
|
);
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
function requireMutationPreflight(input, runtime
|
|
405
|
+
function requireMutationPreflight(input, runtime) {
|
|
406
406
|
const noninteractive = runtime.interaction.machine || runtime.interaction.redirected;
|
|
407
407
|
if (noninteractive && !String(input.idempotencyKey ?? "").trim()) {
|
|
408
408
|
usage(
|
|
@@ -410,23 +410,6 @@ function requireMutationPreflight(input, runtime, { consent = false } = {}) {
|
|
|
410
410
|
"idempotency-key"
|
|
411
411
|
);
|
|
412
412
|
}
|
|
413
|
-
if (noninteractive && consent && input.yes !== true) {
|
|
414
|
-
usage(
|
|
415
|
-
"Consent is required. Review the exact intent and supply --yes.",
|
|
416
|
-
"yes",
|
|
417
|
-
"consent_required"
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
async function reviewed(runtime, options) {
|
|
423
|
-
const accepted = await runtime.interaction.review(options);
|
|
424
|
-
if (accepted) return null;
|
|
425
|
-
return runtime.interaction.result({
|
|
426
|
-
data: null,
|
|
427
|
-
exitCode: 1,
|
|
428
|
-
outcome: { class: "ended", phase: "declined" },
|
|
429
|
-
});
|
|
430
413
|
}
|
|
431
414
|
|
|
432
415
|
const SAFE_EXTERNAL_ERRORS = {
|
|
@@ -493,28 +476,11 @@ async function showBucket(input, runtime) {
|
|
|
493
476
|
|
|
494
477
|
async function connectBucket(input, runtime) {
|
|
495
478
|
const intent = await connectionIntent(input);
|
|
496
|
-
requireMutationPreflight(input, runtime
|
|
479
|
+
requireMutationPreflight(input, runtime);
|
|
497
480
|
const fingerprint =
|
|
498
481
|
intent.accessKeyId && intent.secretAccessKey
|
|
499
482
|
? credentialFingerprint(intent.accessKeyId, intent.secretAccessKey)
|
|
500
483
|
: null;
|
|
501
|
-
const declined = await reviewed(runtime, {
|
|
502
|
-
title: "Connect external bucket",
|
|
503
|
-
terms: [
|
|
504
|
-
{ label: "Kind", value: providerName(intent.kind) },
|
|
505
|
-
{ label: "Name", value: intent.name },
|
|
506
|
-
{ label: "Bucket", value: intent.bucket },
|
|
507
|
-
{ label: "Prefix", value: intent.prefix ?? "(bucket root)" },
|
|
508
|
-
{ label: "Access", value: intent.readOnly ? "read-only" : "read-write" },
|
|
509
|
-
{
|
|
510
|
-
label: "Credentials",
|
|
511
|
-
value: fingerprint ? `fingerprint ${fingerprint.slice(0, 12)}` : "ambient GCS access",
|
|
512
|
-
},
|
|
513
|
-
{ label: "Verification", value: "separate command required" },
|
|
514
|
-
],
|
|
515
|
-
prompt: "Connect this external bucket?",
|
|
516
|
-
});
|
|
517
|
-
if (declined) return declined;
|
|
518
484
|
|
|
519
485
|
const rows = await fetchDrives(runtime);
|
|
520
486
|
const sameName = bucketRows(rows).filter(
|
|
@@ -644,7 +610,7 @@ async function updateCredentials(input, runtime) {
|
|
|
644
610
|
);
|
|
645
611
|
const accessKeyId = requiredText(input.accessKeyId, "--access-key-id", "access-key-id");
|
|
646
612
|
const secret = await readCredentialFile(input.secretAccessKeyFile);
|
|
647
|
-
requireMutationPreflight(input, runtime
|
|
613
|
+
requireMutationPreflight(input, runtime);
|
|
648
614
|
const fingerprint = credentialFingerprint(accessKeyId, secret);
|
|
649
615
|
const source = resolveExternalBucket(await fetchDrives(runtime), reference);
|
|
650
616
|
const kind = externalKind(source);
|
|
@@ -655,17 +621,6 @@ async function updateCredentials(input, runtime) {
|
|
|
655
621
|
);
|
|
656
622
|
}
|
|
657
623
|
const id = safeIdentifier(source.id);
|
|
658
|
-
const declined = await reviewed(runtime, {
|
|
659
|
-
title: "Update external bucket credentials",
|
|
660
|
-
terms: [
|
|
661
|
-
{ label: "External bucket", value: `${safeName(source.name)} (${id})` },
|
|
662
|
-
{ label: "Kind", value: providerName(kind) },
|
|
663
|
-
{ label: "Credential fingerprint", value: fingerprint.slice(0, 12) },
|
|
664
|
-
{ label: "Verification", value: "must be run again" },
|
|
665
|
-
],
|
|
666
|
-
prompt: "Replace these credentials?",
|
|
667
|
-
});
|
|
668
|
-
if (declined) return declined;
|
|
669
624
|
let updated;
|
|
670
625
|
try {
|
|
671
626
|
updated = await runtime.fetch(
|
|
@@ -698,20 +653,9 @@ async function updateCredentials(input, runtime) {
|
|
|
698
653
|
|
|
699
654
|
async function disconnectBucket(input, runtime) {
|
|
700
655
|
const [reference] = positions(input, 1, "ornn external-buckets disconnect <external-bucket>");
|
|
701
|
-
requireMutationPreflight(input, runtime
|
|
656
|
+
requireMutationPreflight(input, runtime);
|
|
702
657
|
const source = resolveExternalBucket(await fetchDrives(runtime), reference);
|
|
703
658
|
const safe = safeExternalBucket(source);
|
|
704
|
-
const declined = await reviewed(runtime, {
|
|
705
|
-
title: "Disconnect external bucket",
|
|
706
|
-
terms: [
|
|
707
|
-
{ label: "External bucket", value: `${safe.name} (${safe.id})` },
|
|
708
|
-
{ label: "Kind", value: providerName(safe.kind) },
|
|
709
|
-
{ label: "Source writeback", value: "disabled" },
|
|
710
|
-
{ label: "Consequence", value: "source access is removed after all mounts are detached" },
|
|
711
|
-
],
|
|
712
|
-
prompt: "Disconnect this external bucket?",
|
|
713
|
-
});
|
|
714
|
-
if (declined) return declined;
|
|
715
659
|
const attachedReservations = activeMountReservations(source);
|
|
716
660
|
if (attachedReservations.length > 0) {
|
|
717
661
|
throw new StorageCommandError(
|
|
@@ -1159,7 +1103,7 @@ async function importBucket(input, runtime) {
|
|
|
1159
1103
|
usage("Noninteractive --wait requires --timeout <duration>.", "timeout");
|
|
1160
1104
|
}
|
|
1161
1105
|
const timeoutMs = parseTimeout(input.timeout);
|
|
1162
|
-
requireMutationPreflight(input, runtime
|
|
1106
|
+
requireMutationPreflight(input, runtime);
|
|
1163
1107
|
|
|
1164
1108
|
const source = resolveExternalBucket(await fetchDrives(runtime), reference);
|
|
1165
1109
|
const bucket = safeExternalBucket(source);
|
|
@@ -1229,18 +1173,6 @@ async function importBucket(input, runtime) {
|
|
|
1229
1173
|
: null;
|
|
1230
1174
|
const replayed = attachment !== null;
|
|
1231
1175
|
if (!attachment) {
|
|
1232
|
-
const declined = await reviewed(runtime, {
|
|
1233
|
-
title: "Import external bucket",
|
|
1234
|
-
terms: [
|
|
1235
|
-
{ label: "Source", value: `${providerName(bucket.kind)} ${bucket.location.bucket}` },
|
|
1236
|
-
{ label: "Working volume", value: `${bucket.name} (${bucket.id})` },
|
|
1237
|
-
{ label: "Reservation", value: reservationId },
|
|
1238
|
-
{ label: "Isolation", value: "one Ornn-managed working copy" },
|
|
1239
|
-
{ label: "Source writeback", value: "disabled" },
|
|
1240
|
-
],
|
|
1241
|
-
prompt: "Create this managed working copy?",
|
|
1242
|
-
});
|
|
1243
|
-
if (declined) return declined;
|
|
1244
1176
|
try {
|
|
1245
1177
|
const payload = await runtime.fetch(
|
|
1246
1178
|
computeEndpoint(
|
|
@@ -18,7 +18,6 @@ const BOOLEAN_OPTIONS = new Set([
|
|
|
18
18
|
"read-write",
|
|
19
19
|
"recursive",
|
|
20
20
|
"wait",
|
|
21
|
-
"yes",
|
|
22
21
|
]);
|
|
23
22
|
|
|
24
23
|
const OPTIONAL_VALUE_OPTIONS = new Set(["resume"]);
|
|
@@ -230,42 +229,8 @@ export function createInteractionRuntime({
|
|
|
230
229
|
return answer;
|
|
231
230
|
}
|
|
232
231
|
|
|
233
|
-
async function review(
|
|
234
|
-
|
|
235
|
-
if (!requireLive && options.yes === true) return true;
|
|
236
|
-
usage(
|
|
237
|
-
requireLive
|
|
238
|
-
? "This operation is supported only in an eligible interactive terminal."
|
|
239
|
-
: "Consent is required. Review the terms and supply --yes to continue.",
|
|
240
|
-
{
|
|
241
|
-
code: requireLive ? "unsupported_in_machine_mode" : "consent_required",
|
|
242
|
-
...(!requireLive ? { field: "yes" } : {}),
|
|
243
|
-
}
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
if (requireLive && !eligibleHuman) {
|
|
247
|
-
usage("This operation is supported only in an eligible interactive terminal.", {
|
|
248
|
-
code: "unsupported_in_machine_mode",
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
stderr.write(`${title}\n`);
|
|
252
|
-
for (const term of terms) stderr.write(` ${term.label}: ${term.value}\n`);
|
|
253
|
-
|
|
254
|
-
if (!requireLive && options.yes === true) return true;
|
|
255
|
-
if (!eligibleHuman) {
|
|
256
|
-
usage("Consent is required. Review the terms and supply --yes to continue.", {
|
|
257
|
-
code: "consent_required",
|
|
258
|
-
field: "yes",
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
let answer;
|
|
262
|
-
try {
|
|
263
|
-
answer = await readline().question(`${prompt} [y/N] `);
|
|
264
|
-
} catch (caught) {
|
|
265
|
-
if (caught?.code === "ERR_USE_AFTER_CLOSE") return false;
|
|
266
|
-
throw caught;
|
|
267
|
-
}
|
|
268
|
-
return /^(y|yes)$/i.test(String(answer ?? "").trim());
|
|
232
|
+
async function review() {
|
|
233
|
+
return true;
|
|
269
234
|
}
|
|
270
235
|
|
|
271
236
|
function webUrl(path) {
|
|
@@ -914,12 +914,6 @@ function clusterMachineLaunchPreflight(runtime, input, membershipCount) {
|
|
|
914
914
|
});
|
|
915
915
|
}
|
|
916
916
|
if (runtime.eligibleHuman) return;
|
|
917
|
-
if (input.yes !== true) {
|
|
918
|
-
runtime.usage("--yes is required for automation after reviewing the exact intent.", {
|
|
919
|
-
code: "consent_required",
|
|
920
|
-
field: "yes",
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
917
|
const references = requestedMachineReferences(input);
|
|
924
918
|
if (membershipCount === null || references.length !== membershipCount) {
|
|
925
919
|
runtime.usage("Automation requires --machines with exactly the --nodes membership count.", {
|
|
@@ -1257,47 +1251,13 @@ export async function launchClusterMode(
|
|
|
1257
1251
|
accessMode
|
|
1258
1252
|
);
|
|
1259
1253
|
if (eligibilitySignature(reviewEligibility) !== eligibilitySignature(initialEligibility)) {
|
|
1260
|
-
runtime.error(`${label} eligibility changed before
|
|
1254
|
+
runtime.error(`${label} eligibility changed before launch; no cluster was created.`, {
|
|
1261
1255
|
code: "cluster_eligibility_changed",
|
|
1262
1256
|
});
|
|
1263
1257
|
}
|
|
1264
|
-
const confirmed = await runtime.review({
|
|
1265
|
-
title: `Launch ${label} cluster`,
|
|
1266
|
-
terms: [
|
|
1267
|
-
{ label: "Organization", value: `${organization.name} (${organization.id})` },
|
|
1268
|
-
{ label: "Capacity reservation", value: reservationId },
|
|
1269
|
-
{ label: "Access mode", value: label },
|
|
1270
|
-
{ label: "Network", value: networkMode },
|
|
1271
|
-
...(keyPlan
|
|
1272
|
-
? [
|
|
1273
|
-
{
|
|
1274
|
-
label: "SSH key",
|
|
1275
|
-
value: `${text(reviewedKey.label) ?? "SSH key"} (${text(reviewedKey.fingerprint) ?? text(reviewedKey.id) ?? "new key"})`,
|
|
1276
|
-
},
|
|
1277
|
-
]
|
|
1278
|
-
: []),
|
|
1279
|
-
{ label: "Cluster membership", value: String(membershipCount) },
|
|
1280
|
-
...selected.map((machine, index) => ({
|
|
1281
|
-
label: `Machine ${index + 1}`,
|
|
1282
|
-
value: `${machine.hostname} (${machine.id})`,
|
|
1283
|
-
})),
|
|
1284
|
-
...virtualMachineIds.map((virtualMachineId, index) => ({
|
|
1285
|
-
label: `VM ${index + 1}`,
|
|
1286
|
-
value: `${virtualMachineId} (SSH keys are set aside while it is in the cluster)`,
|
|
1287
|
-
})),
|
|
1288
|
-
],
|
|
1289
|
-
});
|
|
1290
|
-
if (!confirmed) {
|
|
1291
|
-
return runtime.result({
|
|
1292
|
-
data: { created: false, requested },
|
|
1293
|
-
exitCode: 1,
|
|
1294
|
-
outcome: { class: "ended", phase: "declined" },
|
|
1295
|
-
presentation: { lines: ["No cluster was created."] },
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1298
1258
|
const currentOrganization = await selectedOrganization(context, runtime);
|
|
1299
1259
|
if (currentOrganization.id !== organization.id) {
|
|
1300
|
-
runtime.error("The selected organization changed
|
|
1260
|
+
runtime.error("The selected organization changed before launch; no cluster was created.", {
|
|
1301
1261
|
code: "organization_changed",
|
|
1302
1262
|
exitCode: 2,
|
|
1303
1263
|
});
|
|
@@ -1312,20 +1272,20 @@ export async function launchClusterMode(
|
|
|
1312
1272
|
if (racedExisting) {
|
|
1313
1273
|
const current = await fetchClusterDetail(context.fetch, runtime, racedExisting);
|
|
1314
1274
|
if (current.network_mode && current.network_mode !== networkMode) {
|
|
1315
|
-
runtime.error(`A different ${label} network goal was accepted
|
|
1275
|
+
runtime.error(`A different ${label} network goal was accepted before launch.`, {
|
|
1316
1276
|
code: "cluster_launch_goal_conflict",
|
|
1317
1277
|
});
|
|
1318
1278
|
}
|
|
1319
1279
|
const currentIds = memberIds(current);
|
|
1320
1280
|
if (currentIds.length === 0) {
|
|
1321
1281
|
if (current.lifecycle.phase !== "provisioning") {
|
|
1322
|
-
runtime.error(`A non-recoverable empty ${label} cluster appeared
|
|
1282
|
+
runtime.error(`A non-recoverable empty ${label} cluster appeared before launch.`, {
|
|
1323
1283
|
code: "cluster_launch_goal_conflict",
|
|
1324
1284
|
});
|
|
1325
1285
|
}
|
|
1326
1286
|
} else {
|
|
1327
1287
|
if (!sameMembers(currentIds, goalIds)) {
|
|
1328
|
-
runtime.error(`A different ${label} membership goal was accepted
|
|
1288
|
+
runtime.error(`A different ${label} membership goal was accepted before launch.`, {
|
|
1329
1289
|
code: "cluster_launch_goal_conflict",
|
|
1330
1290
|
});
|
|
1331
1291
|
}
|
|
@@ -1618,21 +1578,6 @@ async function addNode(input, deps) {
|
|
|
1618
1578
|
if (!machine)
|
|
1619
1579
|
deps.interaction.usage("Choose one exact eligible machine.", { code: "machine_not_eligible" });
|
|
1620
1580
|
const goalIds = [...initialMembers, machine.id].sort();
|
|
1621
|
-
const confirmed = await deps.interaction.review({
|
|
1622
|
-
title: `Add ${clusterModeLabel(prepared.cluster.access_mode)} cluster node`,
|
|
1623
|
-
terms: [
|
|
1624
|
-
{ label: "Cluster", value: prepared.cluster.id },
|
|
1625
|
-
{ label: "Machine", value: `${machine.hostname} (${machine.id})` },
|
|
1626
|
-
{ label: "Membership goal", value: String(goalIds.length) },
|
|
1627
|
-
],
|
|
1628
|
-
});
|
|
1629
|
-
if (!confirmed)
|
|
1630
|
-
return deps.interaction.result({
|
|
1631
|
-
data: { changed: false, cluster: prepared.cluster },
|
|
1632
|
-
exitCode: 1,
|
|
1633
|
-
outcome: { class: "ended", phase: "declined" },
|
|
1634
|
-
presentation: { lines: ["Cluster membership was not changed."] },
|
|
1635
|
-
});
|
|
1636
1581
|
const current = await fetchClusterDetail(deps.fetch, deps.interaction, prepared.cluster);
|
|
1637
1582
|
const currentIds = memberIds(current);
|
|
1638
1583
|
if (sameMembers(currentIds, goalIds)) {
|
|
@@ -1643,7 +1588,7 @@ async function addNode(input, deps) {
|
|
|
1643
1588
|
});
|
|
1644
1589
|
}
|
|
1645
1590
|
if (!sameMembers(currentIds, initialMembers)) {
|
|
1646
|
-
deps.interaction.error("Cluster membership changed
|
|
1591
|
+
deps.interaction.error("Cluster membership changed before adding the node.", {
|
|
1647
1592
|
code: "cluster_membership_changed",
|
|
1648
1593
|
});
|
|
1649
1594
|
}
|
|
@@ -1654,7 +1599,7 @@ async function addNode(input, deps) {
|
|
|
1654
1599
|
current.access_mode
|
|
1655
1600
|
);
|
|
1656
1601
|
if (eligibilitySignature(currentEligibility) !== eligibilitySignature(initialEligibility)) {
|
|
1657
|
-
deps.interaction.error("Eligible machines changed
|
|
1602
|
+
deps.interaction.error("Eligible machines changed before adding the node.", {
|
|
1658
1603
|
code: "cluster_eligibility_changed",
|
|
1659
1604
|
});
|
|
1660
1605
|
}
|
|
@@ -1736,22 +1681,6 @@ async function removeNode(input, deps) {
|
|
|
1736
1681
|
code: "cluster_member_not_found",
|
|
1737
1682
|
});
|
|
1738
1683
|
const goalIds = initialMembers.filter((id) => id !== machine.id);
|
|
1739
|
-
const confirmed = await deps.interaction.review({
|
|
1740
|
-
title: `Remove ${clusterModeLabel(prepared.cluster.access_mode)} cluster node`,
|
|
1741
|
-
terms: [
|
|
1742
|
-
{ label: "Cluster", value: prepared.cluster.id },
|
|
1743
|
-
{ label: "Machine", value: `${machine.hostname} (${machine.id})` },
|
|
1744
|
-
{ label: "Membership goal", value: String(goalIds.length) },
|
|
1745
|
-
{ label: "Consequence", value: "Workloads and native access on this member stop." },
|
|
1746
|
-
],
|
|
1747
|
-
});
|
|
1748
|
-
if (!confirmed)
|
|
1749
|
-
return deps.interaction.result({
|
|
1750
|
-
data: { changed: false, cluster: prepared.cluster },
|
|
1751
|
-
exitCode: 1,
|
|
1752
|
-
outcome: { class: "ended", phase: "declined" },
|
|
1753
|
-
presentation: { lines: ["Cluster membership was not changed."] },
|
|
1754
|
-
});
|
|
1755
1684
|
const current = await fetchClusterDetail(deps.fetch, deps.interaction, prepared.cluster);
|
|
1756
1685
|
const currentIds = memberIds(current);
|
|
1757
1686
|
if (sameMembers(currentIds, goalIds)) {
|
|
@@ -1762,7 +1691,7 @@ async function removeNode(input, deps) {
|
|
|
1762
1691
|
});
|
|
1763
1692
|
}
|
|
1764
1693
|
if (!sameMembers(currentIds, initialMembers)) {
|
|
1765
|
-
deps.interaction.error("Cluster membership changed
|
|
1694
|
+
deps.interaction.error("Cluster membership changed before removing the node.", {
|
|
1766
1695
|
code: "cluster_membership_changed",
|
|
1767
1696
|
});
|
|
1768
1697
|
}
|
|
@@ -1836,29 +1765,9 @@ async function teardownCluster(input, deps) {
|
|
|
1836
1765
|
});
|
|
1837
1766
|
}
|
|
1838
1767
|
const reviewedMembers = memberIds(reviewed);
|
|
1839
|
-
const confirmed = await deps.interaction.review({
|
|
1840
|
-
title: `Tear down ${clusterModeLabel(reviewed.access_mode)} cluster`,
|
|
1841
|
-
terms: [
|
|
1842
|
-
{ label: "Cluster", value: reviewed.id },
|
|
1843
|
-
{ label: "Members", value: String(reviewedMembers.length) },
|
|
1844
|
-
{
|
|
1845
|
-
label: "Native access",
|
|
1846
|
-
value: `${clusterNativeAccess(reviewed.access_mode)} access ends.`,
|
|
1847
|
-
},
|
|
1848
|
-
{ label: "Capacity reservation", value: `${reviewed.reservation_id} remains active.` },
|
|
1849
|
-
{ label: "Charges", value: "Capacity charges continue after cluster teardown." },
|
|
1850
|
-
],
|
|
1851
|
-
});
|
|
1852
|
-
if (!confirmed)
|
|
1853
|
-
return deps.interaction.result({
|
|
1854
|
-
data: { changed: false, cluster: reviewed },
|
|
1855
|
-
exitCode: 1,
|
|
1856
|
-
outcome: { class: "ready", phase: "unchanged" },
|
|
1857
|
-
presentation: { lines: ["The cluster remains active. Capacity charges continue."] },
|
|
1858
|
-
});
|
|
1859
1768
|
const current = await fetchClusterDetail(deps.fetch, deps.interaction, reviewed);
|
|
1860
1769
|
if (!sameMembers(memberIds(current), reviewedMembers)) {
|
|
1861
|
-
deps.interaction.error("Cluster membership changed
|
|
1770
|
+
deps.interaction.error("Cluster membership changed before teardown was submitted.", {
|
|
1862
1771
|
code: "cluster_membership_changed",
|
|
1863
1772
|
});
|
|
1864
1773
|
}
|