@serve.zone/interfaces 32.14.0 → 32.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +18 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/clusternode.d.ts +35 -3
- package/dist_ts/data/clusternode.js +141 -3
- package/dist_ts/data/fleetcutover.d.ts +198 -0
- package/dist_ts/data/fleetcutover.js +570 -1
- package/dist_ts/data/runtimestorageclaim.d.ts +43 -0
- package/dist_ts/data/runtimestorageclaim.js +88 -4
- package/dist_ts/requests/fleetcutover.d.ts +70 -6
- package/dist_ts/requests/runtimesession.d.ts +47 -0
- package/package.json +1 -1
- package/readme.md +80 -16
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/clusternode.ts +209 -2
- package/ts/data/fleetcutover.ts +842 -0
- package/ts/data/runtimestorageclaim.ts +122 -3
- package/ts/requests/fleetcutover.ts +80 -5
- package/ts/requests/runtimesession.ts +57 -0
package/ts/data/fleetcutover.ts
CHANGED
|
@@ -6,6 +6,16 @@ import type {
|
|
|
6
6
|
IRuntimeAuthorityReference,
|
|
7
7
|
} from './runtimeassignment.js';
|
|
8
8
|
import type { IRuntimeSessionBinding } from './runtimesession.js';
|
|
9
|
+
import type { IRuntimeStorageClaim, IRuntimeStorageClaimIntent } from './runtimestorageclaim.js';
|
|
10
|
+
import {
|
|
11
|
+
bindRuntimeStorageClaimToIntent,
|
|
12
|
+
snapshotRuntimeStorageClaim,
|
|
13
|
+
snapshotRuntimeStorageClaimIntent,
|
|
14
|
+
validateRuntimeStorageClaim,
|
|
15
|
+
validateRuntimeStorageClaimIntent,
|
|
16
|
+
} from './runtimestorageclaim.js';
|
|
17
|
+
import type { IAppStoreFilesystemStorageRequest } from '../appstore/types.js';
|
|
18
|
+
import { normalizeStorageRequest } from '../platform/storage.js';
|
|
9
19
|
import type { ISecretVersionReference } from './secret.js';
|
|
10
20
|
import {
|
|
11
21
|
canonicalizeStrictJson,
|
|
@@ -281,6 +291,101 @@ export interface IFleetCutoverVolumeHandoffIntent {
|
|
|
281
291
|
digest: TSha256Digest;
|
|
282
292
|
}
|
|
283
293
|
|
|
294
|
+
export interface IFleetCutoverPersistentVolumeMountDescriptor
|
|
295
|
+
extends IFleetCutoverMountDescriptor {
|
|
296
|
+
type: 'volume';
|
|
297
|
+
classification: 'persistent-volume';
|
|
298
|
+
readOnly: false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Cloudly-owned desired handoff prepared from authenticated legacy observations. Observed Docker
|
|
303
|
+
* source identity is not physical storage lineage; Corestore proves lineage later under the held
|
|
304
|
+
* native attachment fence.
|
|
305
|
+
*/
|
|
306
|
+
export interface IFleetCutoverPreparedVolumeHandoffIntent {
|
|
307
|
+
id: string;
|
|
308
|
+
generation: number;
|
|
309
|
+
clusterId: string;
|
|
310
|
+
nodeId: string;
|
|
311
|
+
serviceId: string;
|
|
312
|
+
volumeId: string;
|
|
313
|
+
observedMount: IFleetCutoverPersistentVolumeMountDescriptor;
|
|
314
|
+
claimIntent: IRuntimeStorageClaimIntent;
|
|
315
|
+
runtime: IClusterRuntimeReference & { phase: 'draining' };
|
|
316
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
317
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
318
|
+
legacyStorageEvidence: IFleetCutoverAcceptedEvidenceReference;
|
|
319
|
+
sourceNodeObservation: IFleetCutoverAcceptedEvidenceReference;
|
|
320
|
+
acceptedAt: number;
|
|
321
|
+
digest: TSha256Digest;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export interface IFleetCutoverPreparedVolumeHandoffUpsert {
|
|
325
|
+
nodeId: string;
|
|
326
|
+
serviceId: string;
|
|
327
|
+
mountIdentityDigest: TSha256Digest;
|
|
328
|
+
request: IAppStoreFilesystemStorageRequest & {
|
|
329
|
+
accessMode: 'ReadWriteOnce';
|
|
330
|
+
reclaimPolicy: 'retain';
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** One bounded CAS mutation; the durable aggregate may contain many such 32-entry batches. */
|
|
335
|
+
export interface IFleetCutoverPreparedVolumeHandoffMutation {
|
|
336
|
+
mutationId: string;
|
|
337
|
+
clusterId: string;
|
|
338
|
+
currentRuntime: IClusterRuntimeReference & { phase: 'draining' };
|
|
339
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
340
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
341
|
+
expectedAggregate: IFleetCutoverAcceptedEvidenceReference | null;
|
|
342
|
+
upserts: IFleetCutoverPreparedVolumeHandoffUpsert[];
|
|
343
|
+
removals: IFleetCutoverAcceptedEvidenceReference[];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Revisioned header for one complete expected set. It intentionally contains no page references:
|
|
348
|
+
* immutable pages bind back to this exact revision, avoiding an aggregate/page digest cycle.
|
|
349
|
+
*/
|
|
350
|
+
export interface IFleetCutoverPreparedVolumeHandoffAggregate {
|
|
351
|
+
id: string;
|
|
352
|
+
generation: number;
|
|
353
|
+
previous: IFleetCutoverAcceptedEvidenceReference | null;
|
|
354
|
+
clusterId: string;
|
|
355
|
+
runtime: IClusterRuntimeReference & { phase: 'draining' };
|
|
356
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
357
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
358
|
+
state: 'incomplete' | 'complete';
|
|
359
|
+
requiredHandoffCount: number;
|
|
360
|
+
handoffCount: number;
|
|
361
|
+
pageCount: number;
|
|
362
|
+
handoffSetDigest: TSha256Digest;
|
|
363
|
+
acceptedAt: number;
|
|
364
|
+
digest: TSha256Digest;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** One immutable 1..32-entry page pinned to one aggregate revision. */
|
|
368
|
+
export interface IFleetCutoverPreparedVolumeHandoffPage {
|
|
369
|
+
id: string;
|
|
370
|
+
generation: 1;
|
|
371
|
+
aggregate: IFleetCutoverAcceptedEvidenceReference;
|
|
372
|
+
pageIndex: number;
|
|
373
|
+
handoffs: IFleetCutoverPreparedVolumeHandoffPageReference[];
|
|
374
|
+
handoffSetPageDigest: TSha256Digest;
|
|
375
|
+
digest: TSha256Digest;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Immutable page membership commits both the handoff revision and its owning physical node. */
|
|
379
|
+
export interface IFleetCutoverPreparedVolumeHandoffPageReference
|
|
380
|
+
extends IFleetCutoverAcceptedEvidenceReference {
|
|
381
|
+
nodeId: string;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export interface IFleetCutoverPreparedVolumeHandoffPageSetEntry {
|
|
385
|
+
pageIndex: number;
|
|
386
|
+
handoffSetPageDigest: TSha256Digest;
|
|
387
|
+
}
|
|
388
|
+
|
|
284
389
|
interface IFleetCutoverVolumeFenceBase {
|
|
285
390
|
id: string;
|
|
286
391
|
generation: number;
|
|
@@ -328,6 +433,40 @@ export interface IFleetCutoverCorestoreAdoptionReceipt {
|
|
|
328
433
|
digest: TSha256Digest;
|
|
329
434
|
}
|
|
330
435
|
|
|
436
|
+
/** Current-session Cloudly grant for one post-Pallet adoption attempt. */
|
|
437
|
+
export interface IFleetCutoverStorageAdoptionGrant {
|
|
438
|
+
id: string;
|
|
439
|
+
generation: number;
|
|
440
|
+
previous: IFleetCutoverAcceptedEvidenceReference | null;
|
|
441
|
+
clusterId: string;
|
|
442
|
+
nodeId: string;
|
|
443
|
+
serviceId: string;
|
|
444
|
+
volumeId: string;
|
|
445
|
+
observedMount: IFleetCutoverPersistentVolumeMountDescriptor;
|
|
446
|
+
runtime: IClusterRuntimeReference & { phase: 'pallet' };
|
|
447
|
+
session: IRuntimeSessionBinding;
|
|
448
|
+
handoffIntent: IFleetCutoverAcceptedEvidenceReference;
|
|
449
|
+
palletFence: IFleetCutoverAcceptedEvidenceReference;
|
|
450
|
+
acceptedAt: number;
|
|
451
|
+
digest: TSha256Digest;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Exact result Pallet persists only after revalidating the current grant following adoption. */
|
|
455
|
+
export interface IFleetCutoverStorageAdoptionBundle {
|
|
456
|
+
grant: IFleetCutoverStorageAdoptionGrant;
|
|
457
|
+
fence: Extract<TFleetCutoverVolumeFence, { state: 'consumed' }>;
|
|
458
|
+
adoption: IFleetCutoverCorestoreAdoptionReceipt;
|
|
459
|
+
claim: IRuntimeStorageClaim;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** One authenticated Spark reporter's complete bounded subset of a full immutable aggregate page. */
|
|
463
|
+
export interface IFleetCutoverHeldVolumeFenceEvidencePage {
|
|
464
|
+
aggregate: IFleetCutoverAcceptedEvidenceReference;
|
|
465
|
+
page: IFleetCutoverPreparedVolumeHandoffPage;
|
|
466
|
+
nodeId: string;
|
|
467
|
+
fences: Extract<TFleetCutoverVolumeFence, { state: 'held' }>[];
|
|
468
|
+
}
|
|
469
|
+
|
|
331
470
|
export interface IFleetCutoverCoreflowRetirementEvidence {
|
|
332
471
|
id: string;
|
|
333
472
|
generation: number;
|
|
@@ -497,6 +636,11 @@ export const fleetCutoverContract = Object.freeze({
|
|
|
497
636
|
maximumNodes: 1024,
|
|
498
637
|
maximumContainersPerNode: 512,
|
|
499
638
|
maximumMountsPerOwner: 32,
|
|
639
|
+
maximumPreparedVolumeHandoffMutationEntries: 32,
|
|
640
|
+
/** Every admitted task has one node placement and at most 32 mounts: 4096 * 32. */
|
|
641
|
+
maximumPreparedVolumeHandoffs: 4096 * 32,
|
|
642
|
+
maximumPreparedVolumeHandoffPages: (4096 * 32) / 32,
|
|
643
|
+
maximumHeldVolumeFenceEvidenceBytes: 131_072,
|
|
500
644
|
maximumClassifications: 512,
|
|
501
645
|
observationFreshnessMs: 300_000,
|
|
502
646
|
mountSourceIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-source-identity',
|
|
@@ -508,8 +652,16 @@ export const fleetCutoverContract = Object.freeze({
|
|
|
508
652
|
censusDigestDomain: 'serve.zone/fleet-cutover-census',
|
|
509
653
|
scopeDigestDomain: 'serve.zone/fleet-cutover-scope',
|
|
510
654
|
volumeHandoffDigestDomain: 'serve.zone/fleet-cutover-volume-handoff',
|
|
655
|
+
preparedVolumeHandoffDigestDomain: 'serve.zone/fleet-cutover-prepared-volume-handoff',
|
|
656
|
+
preparedVolumeHandoffAggregateDigestDomain:
|
|
657
|
+
'serve.zone/fleet-cutover-prepared-volume-handoff-aggregate',
|
|
658
|
+
preparedVolumeHandoffPageContentDigestDomain:
|
|
659
|
+
'serve.zone/fleet-cutover-prepared-volume-handoff-page-content',
|
|
660
|
+
preparedVolumeHandoffPageDigestDomain: 'serve.zone/fleet-cutover-prepared-volume-handoff-page',
|
|
661
|
+
preparedVolumeHandoffSetDigestDomain: 'serve.zone/fleet-cutover-prepared-volume-handoff-set',
|
|
511
662
|
volumeFenceDigestDomain: 'serve.zone/fleet-cutover-volume-fence',
|
|
512
663
|
corestoreAdoptionDigestDomain: 'serve.zone/fleet-cutover-corestore-adoption',
|
|
664
|
+
storageAdoptionGrantDigestDomain: 'serve.zone/fleet-cutover-storage-adoption-grant',
|
|
513
665
|
coreflowRetirementDigestDomain: 'serve.zone/fleet-cutover-coreflow-retirement',
|
|
514
666
|
ingressDesignationDigestDomain: 'serve.zone/fleet-cutover-ingress-designation',
|
|
515
667
|
ingressAuthorityDigestDomain: 'serve.zone/fleet-cutover-ingress-authority',
|
|
@@ -1053,6 +1205,13 @@ export const fleetCutoverEvidenceReferenceOf = (
|
|
|
1053
1205
|
digest: valueArg.digest,
|
|
1054
1206
|
});
|
|
1055
1207
|
|
|
1208
|
+
export const fleetCutoverPreparedVolumeHandoffPageReferenceOf = (
|
|
1209
|
+
valueArg: { id: string; generation: number; digest: TSha256Digest; nodeId: string },
|
|
1210
|
+
): IFleetCutoverPreparedVolumeHandoffPageReference => ({
|
|
1211
|
+
...fleetCutoverEvidenceReferenceOf(valueArg),
|
|
1212
|
+
nodeId: networkNodeId(valueArg.nodeId),
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1056
1215
|
export const snapshotFleetCutoverAcceptedEvidenceReference = (
|
|
1057
1216
|
valueArg: unknown,
|
|
1058
1217
|
): IFleetCutoverAcceptedEvidenceReference => safe(() => reference(captureNetwork(valueArg)));
|
|
@@ -1209,6 +1368,383 @@ export const computeFleetCutoverVolumeHandoffDigest = async (
|
|
|
1209
1368
|
snapshotFleetCutoverVolumeHandoffIntent(valueArg),
|
|
1210
1369
|
);
|
|
1211
1370
|
|
|
1371
|
+
const persistentVolumeMountDescriptor = (
|
|
1372
|
+
valueArg: unknown,
|
|
1373
|
+
): IFleetCutoverPersistentVolumeMountDescriptor => {
|
|
1374
|
+
const value = mountDescriptor(valueArg);
|
|
1375
|
+
requireNetwork(value.type === 'volume'
|
|
1376
|
+
&& value.classification === 'persistent-volume'
|
|
1377
|
+
&& value.readOnly === false);
|
|
1378
|
+
return value as IFleetCutoverPersistentVolumeMountDescriptor;
|
|
1379
|
+
};
|
|
1380
|
+
|
|
1381
|
+
const preparedVolumeHandoff = (
|
|
1382
|
+
valueArg: unknown,
|
|
1383
|
+
): IFleetCutoverPreparedVolumeHandoffIntent => {
|
|
1384
|
+
const value = networkRecord(valueArg, [
|
|
1385
|
+
'id', 'generation', 'clusterId', 'nodeId', 'serviceId', 'volumeId', 'observedMount',
|
|
1386
|
+
'claimIntent', 'runtime', 'scope', 'targetSet', 'legacyStorageEvidence',
|
|
1387
|
+
'sourceNodeObservation', 'acceptedAt', 'digest',
|
|
1388
|
+
]);
|
|
1389
|
+
const clusterId = networkId(value.clusterId);
|
|
1390
|
+
const nodeId = networkNodeId(value.nodeId);
|
|
1391
|
+
const serviceId = networkId(value.serviceId);
|
|
1392
|
+
const observedMount = persistentVolumeMountDescriptor(value.observedMount);
|
|
1393
|
+
const claimIntent = snapshotRuntimeStorageClaimIntent(value.claimIntent);
|
|
1394
|
+
const runtime = runtimeReference(value.runtime);
|
|
1395
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
1396
|
+
requireNetwork(runtime.id === clusterId && runtime.phase === 'draining'
|
|
1397
|
+
&& targetSet.cloudlyClusterId === clusterId
|
|
1398
|
+
&& claimIntent.clusterId === clusterId
|
|
1399
|
+
&& claimIntent.nodeId === nodeId
|
|
1400
|
+
&& claimIntent.serviceId === serviceId);
|
|
1401
|
+
return {
|
|
1402
|
+
id: networkId(value.id),
|
|
1403
|
+
generation: networkCounter(value.generation),
|
|
1404
|
+
clusterId,
|
|
1405
|
+
nodeId,
|
|
1406
|
+
serviceId,
|
|
1407
|
+
volumeId: networkId(value.volumeId),
|
|
1408
|
+
observedMount,
|
|
1409
|
+
claimIntent,
|
|
1410
|
+
runtime: runtime as IFleetCutoverPreparedVolumeHandoffIntent['runtime'],
|
|
1411
|
+
scope: reference(value.scope),
|
|
1412
|
+
targetSet,
|
|
1413
|
+
legacyStorageEvidence: reference(value.legacyStorageEvidence),
|
|
1414
|
+
sourceNodeObservation: reference(value.sourceNodeObservation),
|
|
1415
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
1416
|
+
digest: networkDigest(value.digest),
|
|
1417
|
+
};
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
export const snapshotFleetCutoverPreparedVolumeHandoffIntent = (
|
|
1421
|
+
valueArg: unknown,
|
|
1422
|
+
): IFleetCutoverPreparedVolumeHandoffIntent => safe(
|
|
1423
|
+
() => preparedVolumeHandoff(captureNetwork(valueArg)),
|
|
1424
|
+
);
|
|
1425
|
+
|
|
1426
|
+
export const computeFleetCutoverPreparedVolumeHandoffDigest = async (
|
|
1427
|
+
valueArg: unknown,
|
|
1428
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1429
|
+
fleetCutoverContract.preparedVolumeHandoffDigestDomain,
|
|
1430
|
+
snapshotFleetCutoverPreparedVolumeHandoffIntent(valueArg),
|
|
1431
|
+
);
|
|
1432
|
+
|
|
1433
|
+
export const validateFleetCutoverPreparedVolumeHandoffIntent = async (
|
|
1434
|
+
valueArg: unknown,
|
|
1435
|
+
): Promise<string[]> => {
|
|
1436
|
+
try {
|
|
1437
|
+
const value = snapshotFleetCutoverPreparedVolumeHandoffIntent(valueArg);
|
|
1438
|
+
if ((await validateRuntimeStorageClaimIntent(value.claimIntent)).length) {
|
|
1439
|
+
return ['fleet cutover prepared volume handoff claim intent is invalid'];
|
|
1440
|
+
}
|
|
1441
|
+
return value.digest === await computeFleetCutoverPreparedVolumeHandoffDigest(value)
|
|
1442
|
+
? [] : ['fleet cutover prepared volume handoff digest does not match'];
|
|
1443
|
+
} catch {
|
|
1444
|
+
return ['fleet cutover prepared volume handoff must use its exact canonical schema'];
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
export const snapshotFleetCutoverPreparedVolumeHandoffMutation = (
|
|
1449
|
+
valueArg: unknown,
|
|
1450
|
+
): IFleetCutoverPreparedVolumeHandoffMutation => safe(() => {
|
|
1451
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
1452
|
+
'mutationId', 'clusterId', 'currentRuntime', 'scope', 'targetSet', 'expectedAggregate',
|
|
1453
|
+
'upserts', 'removals',
|
|
1454
|
+
]);
|
|
1455
|
+
const clusterId = networkId(value.clusterId);
|
|
1456
|
+
const currentRuntime = runtimeReference(value.currentRuntime);
|
|
1457
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
1458
|
+
requireNetwork(currentRuntime.id === clusterId && currentRuntime.phase === 'draining'
|
|
1459
|
+
&& targetSet.cloudlyClusterId === clusterId);
|
|
1460
|
+
const requestKeys = new Set<string>();
|
|
1461
|
+
const placementKeys = new Set<string>();
|
|
1462
|
+
const upserts = networkSorted(networkList(
|
|
1463
|
+
value.upserts,
|
|
1464
|
+
fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
|
|
1465
|
+
itemArg => {
|
|
1466
|
+
const item = networkRecord(itemArg, [
|
|
1467
|
+
'nodeId', 'serviceId', 'mountIdentityDigest', 'request',
|
|
1468
|
+
]);
|
|
1469
|
+
const nodeId = networkNodeId(item.nodeId);
|
|
1470
|
+
const serviceId = networkId(item.serviceId);
|
|
1471
|
+
const mountIdentityDigest = networkDigest(item.mountIdentityDigest);
|
|
1472
|
+
const request = normalizeStorageRequest(item.request);
|
|
1473
|
+
requireNetwork(request.kind === 'filesystem'
|
|
1474
|
+
&& request.accessMode === 'ReadWriteOnce'
|
|
1475
|
+
&& request.reclaimPolicy === 'retain');
|
|
1476
|
+
const requestKey = `${nodeId}\u0000${serviceId}\u0000${request.id}`;
|
|
1477
|
+
const placementKey = `${nodeId}\u0000${serviceId}\u0000${mountIdentityDigest}`;
|
|
1478
|
+
requireNetwork(!requestKeys.has(requestKey) && !placementKeys.has(placementKey));
|
|
1479
|
+
requestKeys.add(requestKey);
|
|
1480
|
+
placementKeys.add(placementKey);
|
|
1481
|
+
return {
|
|
1482
|
+
nodeId,
|
|
1483
|
+
serviceId,
|
|
1484
|
+
mountIdentityDigest,
|
|
1485
|
+
request: request as IFleetCutoverPreparedVolumeHandoffUpsert['request'],
|
|
1486
|
+
};
|
|
1487
|
+
},
|
|
1488
|
+
), itemArg => `${itemArg.nodeId}\u0000${itemArg.serviceId}\u0000${itemArg.mountIdentityDigest}`);
|
|
1489
|
+
const removals = networkSorted(networkList(
|
|
1490
|
+
value.removals,
|
|
1491
|
+
fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
|
|
1492
|
+
reference,
|
|
1493
|
+
), itemArg => `${itemArg.id}\u0000${String(itemArg.generation).padStart(16, '0')}\u0000${itemArg.digest}`);
|
|
1494
|
+
requireNetwork(upserts.length + removals.length > 0
|
|
1495
|
+
&& upserts.length + removals.length
|
|
1496
|
+
<= fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries);
|
|
1497
|
+
return {
|
|
1498
|
+
mutationId: networkId(value.mutationId),
|
|
1499
|
+
clusterId,
|
|
1500
|
+
currentRuntime: currentRuntime as IFleetCutoverPreparedVolumeHandoffMutation['currentRuntime'],
|
|
1501
|
+
scope: reference(value.scope),
|
|
1502
|
+
targetSet,
|
|
1503
|
+
expectedAggregate: value.expectedAggregate === null ? null : reference(value.expectedAggregate),
|
|
1504
|
+
upserts,
|
|
1505
|
+
removals,
|
|
1506
|
+
};
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
const preparedVolumeHandoffAggregate = (
|
|
1510
|
+
valueArg: unknown,
|
|
1511
|
+
): IFleetCutoverPreparedVolumeHandoffAggregate => {
|
|
1512
|
+
const value = networkRecord(valueArg, [
|
|
1513
|
+
'id', 'generation', 'previous', 'clusterId', 'runtime', 'scope', 'targetSet', 'state',
|
|
1514
|
+
'requiredHandoffCount', 'handoffCount', 'pageCount', 'handoffSetDigest', 'acceptedAt',
|
|
1515
|
+
'digest',
|
|
1516
|
+
]);
|
|
1517
|
+
const id = networkId(value.id);
|
|
1518
|
+
const generation = networkCounter(value.generation);
|
|
1519
|
+
const previous = value.previous === null ? null : reference(value.previous);
|
|
1520
|
+
requireNetwork(generation === 1
|
|
1521
|
+
? previous === null
|
|
1522
|
+
: previous?.id === id && previous.generation === generation - 1);
|
|
1523
|
+
const clusterId = networkId(value.clusterId);
|
|
1524
|
+
const runtime = runtimeReference(value.runtime);
|
|
1525
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
1526
|
+
const state = literal(['incomplete', 'complete'] as const, value.state);
|
|
1527
|
+
const requiredHandoffCount = nonNegative(value.requiredHandoffCount);
|
|
1528
|
+
const handoffCount = nonNegative(value.handoffCount);
|
|
1529
|
+
const pageCount = nonNegative(value.pageCount);
|
|
1530
|
+
requireNetwork(runtime.id === clusterId && runtime.phase === 'draining'
|
|
1531
|
+
&& targetSet.cloudlyClusterId === clusterId
|
|
1532
|
+
&& requiredHandoffCount <= fleetCutoverContract.maximumPreparedVolumeHandoffs
|
|
1533
|
+
&& handoffCount <= fleetCutoverContract.maximumPreparedVolumeHandoffs
|
|
1534
|
+
&& pageCount <= fleetCutoverContract.maximumPreparedVolumeHandoffPages
|
|
1535
|
+
&& pageCount === Math.ceil(handoffCount
|
|
1536
|
+
/ fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries)
|
|
1537
|
+
&& (state === 'complete') === (requiredHandoffCount === handoffCount));
|
|
1538
|
+
return {
|
|
1539
|
+
id,
|
|
1540
|
+
generation,
|
|
1541
|
+
previous,
|
|
1542
|
+
clusterId,
|
|
1543
|
+
runtime: runtime as IFleetCutoverPreparedVolumeHandoffAggregate['runtime'],
|
|
1544
|
+
scope: reference(value.scope),
|
|
1545
|
+
targetSet,
|
|
1546
|
+
state,
|
|
1547
|
+
requiredHandoffCount,
|
|
1548
|
+
handoffCount,
|
|
1549
|
+
pageCount,
|
|
1550
|
+
handoffSetDigest: networkDigest(value.handoffSetDigest),
|
|
1551
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
1552
|
+
digest: networkDigest(value.digest),
|
|
1553
|
+
};
|
|
1554
|
+
};
|
|
1555
|
+
|
|
1556
|
+
export const snapshotFleetCutoverPreparedVolumeHandoffAggregate = (
|
|
1557
|
+
valueArg: unknown,
|
|
1558
|
+
): IFleetCutoverPreparedVolumeHandoffAggregate => safe(
|
|
1559
|
+
() => preparedVolumeHandoffAggregate(captureNetwork(valueArg)),
|
|
1560
|
+
);
|
|
1561
|
+
|
|
1562
|
+
export const computeFleetCutoverPreparedVolumeHandoffAggregateDigest = async (
|
|
1563
|
+
valueArg: unknown,
|
|
1564
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1565
|
+
fleetCutoverContract.preparedVolumeHandoffAggregateDigestDomain,
|
|
1566
|
+
snapshotFleetCutoverPreparedVolumeHandoffAggregate(valueArg),
|
|
1567
|
+
);
|
|
1568
|
+
|
|
1569
|
+
export const validateFleetCutoverPreparedVolumeHandoffAggregate = async (
|
|
1570
|
+
valueArg: unknown,
|
|
1571
|
+
): Promise<string[]> => {
|
|
1572
|
+
try {
|
|
1573
|
+
const value = snapshotFleetCutoverPreparedVolumeHandoffAggregate(valueArg);
|
|
1574
|
+
return value.digest === await computeFleetCutoverPreparedVolumeHandoffAggregateDigest(value)
|
|
1575
|
+
? [] : ['fleet cutover prepared volume handoff aggregate digest does not match'];
|
|
1576
|
+
} catch {
|
|
1577
|
+
return ['fleet cutover prepared volume handoff aggregate must use its exact canonical schema'];
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
|
|
1581
|
+
const preparedVolumeHandoffPage = (
|
|
1582
|
+
valueArg: unknown,
|
|
1583
|
+
): IFleetCutoverPreparedVolumeHandoffPage => {
|
|
1584
|
+
const value = networkRecord(valueArg, [
|
|
1585
|
+
'id', 'generation', 'aggregate', 'pageIndex', 'handoffs', 'handoffSetPageDigest', 'digest',
|
|
1586
|
+
]);
|
|
1587
|
+
requireNetwork(value.generation === 1);
|
|
1588
|
+
const handoffs = networkSorted(networkList(
|
|
1589
|
+
value.handoffs,
|
|
1590
|
+
fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
|
|
1591
|
+
itemArg => {
|
|
1592
|
+
const item = networkRecord(itemArg, ['id', 'generation', 'digest', 'nodeId']);
|
|
1593
|
+
return fleetCutoverPreparedVolumeHandoffPageReferenceOf({
|
|
1594
|
+
...reference({ id: item.id, generation: item.generation, digest: item.digest }),
|
|
1595
|
+
nodeId: networkNodeId(item.nodeId),
|
|
1596
|
+
});
|
|
1597
|
+
},
|
|
1598
|
+
), itemArg => `${itemArg.id}\u0000${String(itemArg.generation).padStart(16, '0')}\u0000${itemArg.digest}`);
|
|
1599
|
+
const pageIndex = nonNegative(value.pageIndex);
|
|
1600
|
+
requireNetwork(handoffs.length > 0
|
|
1601
|
+
&& pageIndex < fleetCutoverContract.maximumPreparedVolumeHandoffPages);
|
|
1602
|
+
return {
|
|
1603
|
+
id: networkId(value.id),
|
|
1604
|
+
generation: 1,
|
|
1605
|
+
aggregate: reference(value.aggregate),
|
|
1606
|
+
pageIndex,
|
|
1607
|
+
handoffs,
|
|
1608
|
+
handoffSetPageDigest: networkDigest(value.handoffSetPageDigest),
|
|
1609
|
+
digest: networkDigest(value.digest),
|
|
1610
|
+
};
|
|
1611
|
+
};
|
|
1612
|
+
|
|
1613
|
+
export const snapshotFleetCutoverPreparedVolumeHandoffPage = (
|
|
1614
|
+
valueArg: unknown,
|
|
1615
|
+
): IFleetCutoverPreparedVolumeHandoffPage => safe(
|
|
1616
|
+
() => preparedVolumeHandoffPage(captureNetwork(valueArg)),
|
|
1617
|
+
);
|
|
1618
|
+
|
|
1619
|
+
export const computeFleetCutoverPreparedVolumeHandoffPageContentDigest = async (
|
|
1620
|
+
pageIndexArg: unknown,
|
|
1621
|
+
handoffsArg: unknown,
|
|
1622
|
+
): Promise<TSha256Digest> => {
|
|
1623
|
+
const page = preparedVolumeHandoffPage({
|
|
1624
|
+
id: 'FleetCutoverPreparedVolumeHandoffPage:content',
|
|
1625
|
+
generation: 1,
|
|
1626
|
+
aggregate: { id: 'FleetCutoverPreparedVolumeHandoffAggregate:content', generation: 1,
|
|
1627
|
+
digest: `sha256:${'0'.repeat(64)}` },
|
|
1628
|
+
pageIndex: pageIndexArg,
|
|
1629
|
+
handoffs: handoffsArg,
|
|
1630
|
+
handoffSetPageDigest: `sha256:${'0'.repeat(64)}`,
|
|
1631
|
+
digest: `sha256:${'0'.repeat(64)}`,
|
|
1632
|
+
});
|
|
1633
|
+
return canonicalDigest(fleetCutoverContract.preparedVolumeHandoffPageContentDigestDomain, {
|
|
1634
|
+
pageIndex: page.pageIndex,
|
|
1635
|
+
handoffs: page.handoffs,
|
|
1636
|
+
digest: `sha256:${'0'.repeat(64)}`,
|
|
1637
|
+
});
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
export const computeFleetCutoverPreparedVolumeHandoffPageDigest = async (
|
|
1641
|
+
valueArg: unknown,
|
|
1642
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1643
|
+
fleetCutoverContract.preparedVolumeHandoffPageDigestDomain,
|
|
1644
|
+
snapshotFleetCutoverPreparedVolumeHandoffPage(valueArg),
|
|
1645
|
+
);
|
|
1646
|
+
|
|
1647
|
+
export const validateFleetCutoverPreparedVolumeHandoffPage = async (
|
|
1648
|
+
valueArg: unknown,
|
|
1649
|
+
): Promise<string[]> => {
|
|
1650
|
+
try {
|
|
1651
|
+
const value = snapshotFleetCutoverPreparedVolumeHandoffPage(valueArg);
|
|
1652
|
+
if (value.handoffSetPageDigest
|
|
1653
|
+
!== await computeFleetCutoverPreparedVolumeHandoffPageContentDigest(
|
|
1654
|
+
value.pageIndex,
|
|
1655
|
+
value.handoffs,
|
|
1656
|
+
)) {
|
|
1657
|
+
return ['fleet cutover prepared volume handoff page content digest does not match'];
|
|
1658
|
+
}
|
|
1659
|
+
return value.digest === await computeFleetCutoverPreparedVolumeHandoffPageDigest(value)
|
|
1660
|
+
? [] : ['fleet cutover prepared volume handoff page digest does not match'];
|
|
1661
|
+
} catch {
|
|
1662
|
+
return ['fleet cutover prepared volume handoff page must use its exact canonical schema'];
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
|
|
1666
|
+
export const computeFleetCutoverPreparedVolumeHandoffSetDigest = async (
|
|
1667
|
+
entriesArg: unknown,
|
|
1668
|
+
): Promise<TSha256Digest> => safe(async () => {
|
|
1669
|
+
const entries = networkSorted(networkList(
|
|
1670
|
+
captureNetwork(entriesArg),
|
|
1671
|
+
fleetCutoverContract.maximumPreparedVolumeHandoffPages,
|
|
1672
|
+
itemArg => {
|
|
1673
|
+
const item = networkRecord(itemArg, ['pageIndex', 'handoffSetPageDigest']);
|
|
1674
|
+
return {
|
|
1675
|
+
pageIndex: nonNegative(item.pageIndex),
|
|
1676
|
+
handoffSetPageDigest: networkDigest(item.handoffSetPageDigest),
|
|
1677
|
+
};
|
|
1678
|
+
},
|
|
1679
|
+
), itemArg => String(itemArg.pageIndex).padStart(8, '0'));
|
|
1680
|
+
requireNetwork(entries.every((entryArg, indexArg) => entryArg.pageIndex === indexArg));
|
|
1681
|
+
return canonicalDigest(fleetCutoverContract.preparedVolumeHandoffSetDigestDomain, {
|
|
1682
|
+
entries,
|
|
1683
|
+
digest: `sha256:${'0'.repeat(64)}`,
|
|
1684
|
+
});
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
/**
|
|
1688
|
+
* Full pinned-set proof. The caller must supply every page for the exact aggregate revision; a
|
|
1689
|
+
* node-local subset or a mixed revision can never satisfy this binder.
|
|
1690
|
+
*/
|
|
1691
|
+
export const bindFleetCutoverPreparedVolumeHandoffPages = async (
|
|
1692
|
+
aggregateArg: unknown,
|
|
1693
|
+
pagesArg: unknown,
|
|
1694
|
+
): Promise<{
|
|
1695
|
+
aggregate: IFleetCutoverPreparedVolumeHandoffAggregate;
|
|
1696
|
+
pages: IFleetCutoverPreparedVolumeHandoffPage[];
|
|
1697
|
+
}> => {
|
|
1698
|
+
const aggregate = snapshotFleetCutoverPreparedVolumeHandoffAggregate(aggregateArg);
|
|
1699
|
+
if ((await validateFleetCutoverPreparedVolumeHandoffAggregate(aggregate)).length) fail();
|
|
1700
|
+
const pages = safe(() => {
|
|
1701
|
+
requireNetwork(Array.isArray(pagesArg)
|
|
1702
|
+
&& Object.getPrototypeOf(pagesArg) === Array.prototype
|
|
1703
|
+
&& pagesArg.length <= fleetCutoverContract.maximumPreparedVolumeHandoffPages);
|
|
1704
|
+
const keys = Reflect.ownKeys(pagesArg);
|
|
1705
|
+
requireNetwork(keys.length === pagesArg.length + 1);
|
|
1706
|
+
const result: IFleetCutoverPreparedVolumeHandoffPage[] = [];
|
|
1707
|
+
for (let index = 0; index < pagesArg.length; index++) {
|
|
1708
|
+
const descriptor = Object.getOwnPropertyDescriptor(pagesArg, String(index));
|
|
1709
|
+
requireNetwork(descriptor?.enumerable && Object.hasOwn(descriptor, 'value'));
|
|
1710
|
+
result.push(snapshotFleetCutoverPreparedVolumeHandoffPage(descriptor.value));
|
|
1711
|
+
}
|
|
1712
|
+
return networkSorted(result, itemArg => String(itemArg.pageIndex).padStart(8, '0'));
|
|
1713
|
+
});
|
|
1714
|
+
requireNetwork(pages.length === aggregate.pageCount);
|
|
1715
|
+
let handoffCount = 0;
|
|
1716
|
+
const seen = new Set<string>();
|
|
1717
|
+
let previousHandoffKey: string | null = null;
|
|
1718
|
+
for (const [index, page] of pages.entries()) {
|
|
1719
|
+
requireNetwork(page.pageIndex === index
|
|
1720
|
+
&& page.aggregate.id === aggregate.id
|
|
1721
|
+
&& page.aggregate.generation === aggregate.generation
|
|
1722
|
+
&& page.aggregate.digest === aggregate.digest
|
|
1723
|
+
&& !(await validateFleetCutoverPreparedVolumeHandoffPage(page)).length);
|
|
1724
|
+
const expectedLength = index < pages.length - 1
|
|
1725
|
+
? fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries
|
|
1726
|
+
: aggregate.handoffCount
|
|
1727
|
+
% fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries
|
|
1728
|
+
|| fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries;
|
|
1729
|
+
requireNetwork(page.handoffs.length === expectedLength);
|
|
1730
|
+
handoffCount += page.handoffs.length;
|
|
1731
|
+
for (const handoff of page.handoffs) {
|
|
1732
|
+
const key = `${handoff.id}\u0000${handoff.generation}\u0000${handoff.digest}`;
|
|
1733
|
+
requireNetwork(!seen.has(key) && (previousHandoffKey === null || previousHandoffKey < key));
|
|
1734
|
+
seen.add(key);
|
|
1735
|
+
previousHandoffKey = key;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
const entries = pages.map(page => ({
|
|
1739
|
+
pageIndex: page.pageIndex,
|
|
1740
|
+
handoffSetPageDigest: page.handoffSetPageDigest,
|
|
1741
|
+
}));
|
|
1742
|
+
requireNetwork(handoffCount === aggregate.handoffCount
|
|
1743
|
+
&& aggregate.handoffSetDigest
|
|
1744
|
+
=== await computeFleetCutoverPreparedVolumeHandoffSetDigest(entries));
|
|
1745
|
+
return { aggregate, pages };
|
|
1746
|
+
};
|
|
1747
|
+
|
|
1212
1748
|
const volumeFence = (valueArg: unknown): TFleetCutoverVolumeFence => {
|
|
1213
1749
|
requireNetwork(valueArg !== null && typeof valueArg === 'object');
|
|
1214
1750
|
const state = (valueArg as Record<string, unknown>).state;
|
|
@@ -1324,6 +1860,45 @@ export const bindFleetCutoverAdoptionToVolumeFence = (
|
|
|
1324
1860
|
return { adoption, fence };
|
|
1325
1861
|
});
|
|
1326
1862
|
|
|
1863
|
+
/**
|
|
1864
|
+
* Joins the accepted adoption result to the full handoff and provider-ready claim content.
|
|
1865
|
+
* Native attachment fencing and accepted-record dereferencing remain owner checks.
|
|
1866
|
+
*/
|
|
1867
|
+
export const bindFleetCutoverAdoptionToStorageClaim = async (
|
|
1868
|
+
adoptionArg: unknown,
|
|
1869
|
+
handoffArg: unknown,
|
|
1870
|
+
claimArg: unknown,
|
|
1871
|
+
): Promise<{
|
|
1872
|
+
adoption: IFleetCutoverCorestoreAdoptionReceipt;
|
|
1873
|
+
handoff: IFleetCutoverPreparedVolumeHandoffIntent;
|
|
1874
|
+
claim: IRuntimeStorageClaim;
|
|
1875
|
+
}> => {
|
|
1876
|
+
try {
|
|
1877
|
+
const adoption = snapshotFleetCutoverCorestoreAdoptionReceipt(adoptionArg);
|
|
1878
|
+
const handoff = snapshotFleetCutoverPreparedVolumeHandoffIntent(handoffArg);
|
|
1879
|
+
const claim = snapshotRuntimeStorageClaim(claimArg);
|
|
1880
|
+
if ((await validateFleetCutoverCorestoreAdoptionReceipt(adoption)).length
|
|
1881
|
+
|| (await validateFleetCutoverPreparedVolumeHandoffIntent(handoff)).length
|
|
1882
|
+
|| (await validateRuntimeStorageClaim(claim)).length) fail();
|
|
1883
|
+
await bindRuntimeStorageClaimToIntent(claim, handoff.claimIntent);
|
|
1884
|
+
requireNetwork(adoption.clusterId === handoff.clusterId
|
|
1885
|
+
&& adoption.nodeId === handoff.nodeId
|
|
1886
|
+
&& adoption.serviceId === handoff.serviceId
|
|
1887
|
+
&& adoption.volumeId === handoff.volumeId
|
|
1888
|
+
&& adoption.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
|
|
1889
|
+
&& adoption.handoffIntent.id === handoff.id
|
|
1890
|
+
&& adoption.handoffIntent.generation === handoff.generation
|
|
1891
|
+
&& adoption.handoffIntent.digest === handoff.digest
|
|
1892
|
+
&& adoption.claim.id === claim.id
|
|
1893
|
+
&& adoption.claim.generation === claim.generation
|
|
1894
|
+
&& adoption.claim.digest === claim.digest
|
|
1895
|
+
&& adoption.acceptedAt >= handoff.acceptedAt);
|
|
1896
|
+
return { adoption, handoff, claim };
|
|
1897
|
+
} catch {
|
|
1898
|
+
return fail();
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1327
1902
|
export const validateFleetCutoverVolumeHandoffIntent = async (valueArg: unknown): Promise<string[]> => {
|
|
1328
1903
|
try {
|
|
1329
1904
|
const value = snapshotFleetCutoverVolumeHandoffIntent(valueArg);
|
|
@@ -1373,6 +1948,273 @@ const runtimeSession = (valueArg: unknown): IRuntimeSessionBinding => {
|
|
|
1373
1948
|
};
|
|
1374
1949
|
};
|
|
1375
1950
|
|
|
1951
|
+
const storageAdoptionGrant = (valueArg: unknown): IFleetCutoverStorageAdoptionGrant => {
|
|
1952
|
+
const value = networkRecord(valueArg, [
|
|
1953
|
+
'id', 'generation', 'previous', 'clusterId', 'nodeId', 'serviceId', 'volumeId',
|
|
1954
|
+
'observedMount', 'runtime', 'session', 'handoffIntent', 'palletFence', 'acceptedAt',
|
|
1955
|
+
'digest',
|
|
1956
|
+
]);
|
|
1957
|
+
const id = networkId(value.id);
|
|
1958
|
+
const generation = networkCounter(value.generation);
|
|
1959
|
+
const previous = value.previous === null ? null : reference(value.previous);
|
|
1960
|
+
requireNetwork(generation === 1
|
|
1961
|
+
? previous === null
|
|
1962
|
+
: previous?.id === id && previous.generation === generation - 1);
|
|
1963
|
+
const clusterId = networkId(value.clusterId);
|
|
1964
|
+
const nodeId = networkNodeId(value.nodeId);
|
|
1965
|
+
const runtime = runtimeReference(value.runtime);
|
|
1966
|
+
const session = runtimeSession(value.session);
|
|
1967
|
+
requireNetwork(runtime.id === clusterId && runtime.phase === 'pallet'
|
|
1968
|
+
&& session.clusterId === clusterId
|
|
1969
|
+
&& session.nodeId === nodeId
|
|
1970
|
+
&& session.controller.kind === 'cloudly');
|
|
1971
|
+
return {
|
|
1972
|
+
id,
|
|
1973
|
+
generation,
|
|
1974
|
+
previous,
|
|
1975
|
+
clusterId,
|
|
1976
|
+
nodeId,
|
|
1977
|
+
serviceId: networkId(value.serviceId),
|
|
1978
|
+
volumeId: networkId(value.volumeId),
|
|
1979
|
+
observedMount: persistentVolumeMountDescriptor(value.observedMount),
|
|
1980
|
+
runtime: runtime as IFleetCutoverStorageAdoptionGrant['runtime'],
|
|
1981
|
+
session,
|
|
1982
|
+
handoffIntent: reference(value.handoffIntent),
|
|
1983
|
+
palletFence: reference(value.palletFence),
|
|
1984
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
1985
|
+
digest: networkDigest(value.digest),
|
|
1986
|
+
};
|
|
1987
|
+
};
|
|
1988
|
+
|
|
1989
|
+
export const snapshotFleetCutoverStorageAdoptionGrant = (
|
|
1990
|
+
valueArg: unknown,
|
|
1991
|
+
): IFleetCutoverStorageAdoptionGrant => safe(
|
|
1992
|
+
() => storageAdoptionGrant(captureNetwork(valueArg)),
|
|
1993
|
+
);
|
|
1994
|
+
|
|
1995
|
+
export const computeFleetCutoverStorageAdoptionGrantDigest = async (
|
|
1996
|
+
valueArg: unknown,
|
|
1997
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1998
|
+
fleetCutoverContract.storageAdoptionGrantDigestDomain,
|
|
1999
|
+
snapshotFleetCutoverStorageAdoptionGrant(valueArg),
|
|
2000
|
+
);
|
|
2001
|
+
|
|
2002
|
+
export const validateFleetCutoverStorageAdoptionGrant = async (
|
|
2003
|
+
valueArg: unknown,
|
|
2004
|
+
): Promise<string[]> => {
|
|
2005
|
+
try {
|
|
2006
|
+
const value = snapshotFleetCutoverStorageAdoptionGrant(valueArg);
|
|
2007
|
+
return value.digest === await computeFleetCutoverStorageAdoptionGrantDigest(value)
|
|
2008
|
+
? [] : ['fleet cutover storage adoption grant digest does not match'];
|
|
2009
|
+
} catch {
|
|
2010
|
+
return ['fleet cutover storage adoption grant must use its exact canonical schema'];
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
|
|
2014
|
+
/** Content join only; the controller must still prove this is the live current session and phase. */
|
|
2015
|
+
export const bindFleetCutoverStorageAdoptionGrant = async (
|
|
2016
|
+
grantArg: unknown,
|
|
2017
|
+
handoffArg: unknown,
|
|
2018
|
+
heldFenceArg: unknown,
|
|
2019
|
+
): Promise<{
|
|
2020
|
+
grant: IFleetCutoverStorageAdoptionGrant;
|
|
2021
|
+
handoff: IFleetCutoverPreparedVolumeHandoffIntent;
|
|
2022
|
+
fence: Extract<TFleetCutoverVolumeFence, { state: 'held' }>;
|
|
2023
|
+
}> => {
|
|
2024
|
+
try {
|
|
2025
|
+
const grant = snapshotFleetCutoverStorageAdoptionGrant(grantArg);
|
|
2026
|
+
const handoff = snapshotFleetCutoverPreparedVolumeHandoffIntent(handoffArg);
|
|
2027
|
+
const fence = snapshotFleetCutoverVolumeFence(heldFenceArg);
|
|
2028
|
+
if ((await validateFleetCutoverStorageAdoptionGrant(grant)).length
|
|
2029
|
+
|| (await validateFleetCutoverPreparedVolumeHandoffIntent(handoff)).length
|
|
2030
|
+
|| (await validateFleetCutoverVolumeFence(fence)).length) fail();
|
|
2031
|
+
requireNetwork(fence.state === 'held'
|
|
2032
|
+
&& grant.clusterId === handoff.clusterId
|
|
2033
|
+
&& grant.nodeId === handoff.nodeId
|
|
2034
|
+
&& grant.serviceId === handoff.serviceId
|
|
2035
|
+
&& grant.volumeId === handoff.volumeId
|
|
2036
|
+
&& grant.observedMount.type === handoff.observedMount.type
|
|
2037
|
+
&& grant.observedMount.classification === handoff.observedMount.classification
|
|
2038
|
+
&& grant.observedMount.sourceIdentityDigest === handoff.observedMount.sourceIdentityDigest
|
|
2039
|
+
&& grant.observedMount.targetIdentityDigest === handoff.observedMount.targetIdentityDigest
|
|
2040
|
+
&& grant.observedMount.readOnly === handoff.observedMount.readOnly
|
|
2041
|
+
&& grant.observedMount.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
|
|
2042
|
+
&& grant.handoffIntent.id === handoff.id
|
|
2043
|
+
&& grant.handoffIntent.generation === handoff.generation
|
|
2044
|
+
&& grant.handoffIntent.digest === handoff.digest
|
|
2045
|
+
&& grant.palletFence.id === fence.id
|
|
2046
|
+
&& grant.palletFence.generation === fence.generation
|
|
2047
|
+
&& grant.palletFence.digest === fence.digest
|
|
2048
|
+
&& fence.clusterId === handoff.clusterId
|
|
2049
|
+
&& fence.nodeId === handoff.nodeId
|
|
2050
|
+
&& fence.serviceId === handoff.serviceId
|
|
2051
|
+
&& fence.volumeId === handoff.volumeId
|
|
2052
|
+
&& fence.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
|
|
2053
|
+
&& fence.handoffIntent.id === handoff.id
|
|
2054
|
+
&& fence.handoffIntent.generation === handoff.generation
|
|
2055
|
+
&& fence.handoffIntent.digest === handoff.digest
|
|
2056
|
+
&& fence.heldRuntime.id === handoff.runtime.id
|
|
2057
|
+
&& fence.heldRuntime.phase === handoff.runtime.phase
|
|
2058
|
+
&& fence.heldRuntime.generation === handoff.runtime.generation
|
|
2059
|
+
&& grant.session.controller.id === handoff.claimIntent.controller.id
|
|
2060
|
+
&& grant.session.controller.epoch === handoff.claimIntent.controller.epoch
|
|
2061
|
+
&& grant.session.runtimeNamespace === handoff.claimIntent.runtimeNamespace
|
|
2062
|
+
&& grant.acceptedAt >= fence.heldAt);
|
|
2063
|
+
return { grant, handoff, fence };
|
|
2064
|
+
} catch {
|
|
2065
|
+
return fail();
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
|
|
2069
|
+
export const snapshotFleetCutoverStorageAdoptionBundle = (
|
|
2070
|
+
valueArg: unknown,
|
|
2071
|
+
): IFleetCutoverStorageAdoptionBundle => safe(() => {
|
|
2072
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
2073
|
+
'grant', 'fence', 'adoption', 'claim',
|
|
2074
|
+
]);
|
|
2075
|
+
const fence = snapshotFleetCutoverVolumeFence(value.fence);
|
|
2076
|
+
requireNetwork(fence.state === 'consumed');
|
|
2077
|
+
return {
|
|
2078
|
+
grant: snapshotFleetCutoverStorageAdoptionGrant(value.grant),
|
|
2079
|
+
fence,
|
|
2080
|
+
adoption: snapshotFleetCutoverCorestoreAdoptionReceipt(value.adoption),
|
|
2081
|
+
claim: snapshotRuntimeStorageClaim(value.claim),
|
|
2082
|
+
};
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* Binds the complete post-adoption content. Physical lineage remains the provider-produced
|
|
2087
|
+
* adoption digest and is deliberately never equated to the observed Docker source identity.
|
|
2088
|
+
*/
|
|
2089
|
+
export const bindFleetCutoverStorageAdoptionBundle = async (
|
|
2090
|
+
valueArg: unknown,
|
|
2091
|
+
handoffArg: unknown,
|
|
2092
|
+
): Promise<{
|
|
2093
|
+
bundle: IFleetCutoverStorageAdoptionBundle;
|
|
2094
|
+
handoff: IFleetCutoverPreparedVolumeHandoffIntent;
|
|
2095
|
+
}> => {
|
|
2096
|
+
try {
|
|
2097
|
+
const bundle = snapshotFleetCutoverStorageAdoptionBundle(valueArg);
|
|
2098
|
+
const handoff = snapshotFleetCutoverPreparedVolumeHandoffIntent(handoffArg);
|
|
2099
|
+
const { grant, fence, adoption, claim } = bundle;
|
|
2100
|
+
if ((await validateFleetCutoverStorageAdoptionGrant(grant)).length
|
|
2101
|
+
|| (await validateFleetCutoverPreparedVolumeHandoffIntent(handoff)).length
|
|
2102
|
+
|| (await validateFleetCutoverVolumeFence(fence)).length
|
|
2103
|
+
|| (await validateFleetCutoverCorestoreAdoptionReceipt(adoption)).length
|
|
2104
|
+
|| (await validateRuntimeStorageClaim(claim)).length) fail();
|
|
2105
|
+
await bindRuntimeStorageClaimToIntent(claim, handoff.claimIntent);
|
|
2106
|
+
requireNetwork(grant.clusterId === handoff.clusterId
|
|
2107
|
+
&& grant.nodeId === handoff.nodeId
|
|
2108
|
+
&& grant.serviceId === handoff.serviceId
|
|
2109
|
+
&& grant.volumeId === handoff.volumeId
|
|
2110
|
+
&& grant.observedMount.type === handoff.observedMount.type
|
|
2111
|
+
&& grant.observedMount.classification === handoff.observedMount.classification
|
|
2112
|
+
&& grant.observedMount.sourceIdentityDigest === handoff.observedMount.sourceIdentityDigest
|
|
2113
|
+
&& grant.observedMount.targetIdentityDigest === handoff.observedMount.targetIdentityDigest
|
|
2114
|
+
&& grant.observedMount.readOnly === handoff.observedMount.readOnly
|
|
2115
|
+
&& grant.observedMount.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
|
|
2116
|
+
&& grant.session.controller.id === handoff.claimIntent.controller.id
|
|
2117
|
+
&& grant.session.controller.epoch === handoff.claimIntent.controller.epoch
|
|
2118
|
+
&& grant.session.runtimeNamespace === handoff.claimIntent.runtimeNamespace
|
|
2119
|
+
&& grant.handoffIntent.id === handoff.id
|
|
2120
|
+
&& grant.handoffIntent.generation === handoff.generation
|
|
2121
|
+
&& grant.handoffIntent.digest === handoff.digest
|
|
2122
|
+
&& fence.clusterId === grant.clusterId
|
|
2123
|
+
&& fence.nodeId === grant.nodeId
|
|
2124
|
+
&& fence.serviceId === grant.serviceId
|
|
2125
|
+
&& fence.volumeId === grant.volumeId
|
|
2126
|
+
&& fence.mountIdentityDigest === grant.observedMount.mountIdentityDigest
|
|
2127
|
+
&& fence.handoffIntent.id === handoff.id
|
|
2128
|
+
&& fence.handoffIntent.generation === handoff.generation
|
|
2129
|
+
&& fence.handoffIntent.digest === handoff.digest
|
|
2130
|
+
&& fence.previous !== null
|
|
2131
|
+
&& fence.previous.id === grant.palletFence.id
|
|
2132
|
+
&& fence.previous.generation === grant.palletFence.generation
|
|
2133
|
+
&& fence.previous.digest === grant.palletFence.digest
|
|
2134
|
+
&& fence.consumedRuntime.id === grant.runtime.id
|
|
2135
|
+
&& fence.consumedRuntime.phase === grant.runtime.phase
|
|
2136
|
+
&& fence.consumedRuntime.generation === grant.runtime.generation
|
|
2137
|
+
&& adoption.clusterId === handoff.clusterId
|
|
2138
|
+
&& adoption.nodeId === handoff.nodeId
|
|
2139
|
+
&& adoption.serviceId === handoff.serviceId
|
|
2140
|
+
&& adoption.volumeId === handoff.volumeId
|
|
2141
|
+
&& adoption.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
|
|
2142
|
+
&& adoption.handoffIntent.id === handoff.id
|
|
2143
|
+
&& adoption.handoffIntent.generation === handoff.generation
|
|
2144
|
+
&& adoption.handoffIntent.digest === handoff.digest
|
|
2145
|
+
&& adoption.palletFence.id === grant.palletFence.id
|
|
2146
|
+
&& adoption.palletFence.generation === grant.palletFence.generation
|
|
2147
|
+
&& adoption.palletFence.digest === grant.palletFence.digest
|
|
2148
|
+
&& adoption.claim.id === claim.id
|
|
2149
|
+
&& adoption.claim.generation === claim.generation
|
|
2150
|
+
&& adoption.claim.digest === claim.digest
|
|
2151
|
+
&& fence.adoptionReceipt.id === adoption.id
|
|
2152
|
+
&& fence.adoptionReceipt.generation === adoption.generation
|
|
2153
|
+
&& fence.adoptionReceipt.digest === adoption.digest
|
|
2154
|
+
&& fence.claim.id === claim.id
|
|
2155
|
+
&& fence.claim.generation === claim.generation
|
|
2156
|
+
&& fence.claim.digest === claim.digest
|
|
2157
|
+
&& adoption.acceptedAt >= grant.acceptedAt
|
|
2158
|
+
&& fence.consumedAt >= adoption.acceptedAt);
|
|
2159
|
+
return { bundle, handoff };
|
|
2160
|
+
} catch {
|
|
2161
|
+
return fail();
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
|
|
2165
|
+
export const snapshotFleetCutoverHeldVolumeFenceEvidencePage = (
|
|
2166
|
+
valueArg: unknown,
|
|
2167
|
+
): IFleetCutoverHeldVolumeFenceEvidencePage => safe(() => {
|
|
2168
|
+
const value = networkRecord(captureNetwork(valueArg), ['aggregate', 'page', 'nodeId', 'fences']);
|
|
2169
|
+
const aggregate = reference(value.aggregate);
|
|
2170
|
+
const page = preparedVolumeHandoffPage(value.page);
|
|
2171
|
+
const nodeId = networkNodeId(value.nodeId);
|
|
2172
|
+
requireNetwork(page.aggregate.id === aggregate.id
|
|
2173
|
+
&& page.aggregate.generation === aggregate.generation
|
|
2174
|
+
&& page.aggregate.digest === aggregate.digest);
|
|
2175
|
+
const pageHandoffs = page.handoffs.filter(itemArg => itemArg.nodeId === nodeId);
|
|
2176
|
+
const fences = networkSorted(networkList(
|
|
2177
|
+
value.fences,
|
|
2178
|
+
fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
|
|
2179
|
+
itemArg => {
|
|
2180
|
+
const fence = volumeFence(itemArg);
|
|
2181
|
+
requireNetwork(fence.state === 'held' && fence.nodeId === nodeId);
|
|
2182
|
+
return fence;
|
|
2183
|
+
},
|
|
2184
|
+
), itemArg => `${itemArg.handoffIntent.id}\u0000${itemArg.handoffIntent.generation}\u0000${itemArg.handoffIntent.digest}`);
|
|
2185
|
+
requireNetwork(fences.length === pageHandoffs.length
|
|
2186
|
+
&& fences.every((fenceArg, indexArg) => {
|
|
2187
|
+
const handoff = pageHandoffs[indexArg];
|
|
2188
|
+
return fenceArg.handoffIntent.id === handoff.id
|
|
2189
|
+
&& fenceArg.handoffIntent.generation === handoff.generation
|
|
2190
|
+
&& fenceArg.handoffIntent.digest === handoff.digest;
|
|
2191
|
+
}));
|
|
2192
|
+
return { aggregate, page, nodeId, fences };
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
export const validateFleetCutoverHeldVolumeFenceEvidencePage = async (
|
|
2196
|
+
valueArg: unknown,
|
|
2197
|
+
): Promise<string[]> => {
|
|
2198
|
+
try {
|
|
2199
|
+
const value = snapshotFleetCutoverHeldVolumeFenceEvidencePage(valueArg);
|
|
2200
|
+
if (new TextEncoder().encode(canonicalizeStrictJson(value, fail)).byteLength
|
|
2201
|
+
> fleetCutoverContract.maximumHeldVolumeFenceEvidenceBytes) {
|
|
2202
|
+
return ['fleet cutover held volume fence evidence page exceeds its encoded byte bound'];
|
|
2203
|
+
}
|
|
2204
|
+
if ((await validateFleetCutoverPreparedVolumeHandoffPage(value.page)).length) {
|
|
2205
|
+
return ['fleet cutover held volume fence evidence page is not a valid immutable page'];
|
|
2206
|
+
}
|
|
2207
|
+
for (const fence of value.fences) {
|
|
2208
|
+
if ((await validateFleetCutoverVolumeFence(fence)).length) {
|
|
2209
|
+
return ['fleet cutover held volume fence evidence contains an invalid held fence'];
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
return [];
|
|
2213
|
+
} catch {
|
|
2214
|
+
return ['fleet cutover held volume fence evidence page must use its exact canonical schema'];
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
|
|
1376
2218
|
export const snapshotFleetCutoverCoreflowRetirementEvidence = (
|
|
1377
2219
|
valueArg: unknown,
|
|
1378
2220
|
): IFleetCutoverCoreflowRetirementEvidence => safe(() => {
|