@serve.zone/interfaces 32.13.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 +30 -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 +248 -0
- package/dist_ts/data/fleetcutover.js +648 -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 +87 -6
- package/dist_ts/requests/runtimesession.d.ts +47 -0
- package/package.json +1 -1
- package/readme.md +108 -18
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/clusternode.ts +209 -2
- package/ts/data/fleetcutover.ts +978 -0
- package/ts/data/runtimestorageclaim.ts +122 -3
- package/ts/requests/fleetcutover.ts +104 -5
- package/ts/requests/runtimesession.ts +57 -0
package/ts/data/fleetcutover.ts
CHANGED
|
@@ -6,6 +6,17 @@ 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';
|
|
19
|
+
import type { ISecretVersionReference } from './secret.js';
|
|
9
20
|
import {
|
|
10
21
|
canonicalizeStrictJson,
|
|
11
22
|
createCanonicalJsonSha256Hex,
|
|
@@ -280,6 +291,101 @@ export interface IFleetCutoverVolumeHandoffIntent {
|
|
|
280
291
|
digest: TSha256Digest;
|
|
281
292
|
}
|
|
282
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
|
+
|
|
283
389
|
interface IFleetCutoverVolumeFenceBase {
|
|
284
390
|
id: string;
|
|
285
391
|
generation: number;
|
|
@@ -327,6 +433,40 @@ export interface IFleetCutoverCorestoreAdoptionReceipt {
|
|
|
327
433
|
digest: TSha256Digest;
|
|
328
434
|
}
|
|
329
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
|
+
|
|
330
470
|
export interface IFleetCutoverCoreflowRetirementEvidence {
|
|
331
471
|
id: string;
|
|
332
472
|
generation: number;
|
|
@@ -373,6 +513,49 @@ export interface IFleetCutoverCoreflowRetirementEvidence {
|
|
|
373
513
|
digest: TSha256Digest;
|
|
374
514
|
}
|
|
375
515
|
|
|
516
|
+
/**
|
|
517
|
+
* Administrator intent to prepare the one explicitly designated ingress service. The caller
|
|
518
|
+
* supplies only compare-and-swap inputs; Cloudly owns credential generation and acceptance.
|
|
519
|
+
*/
|
|
520
|
+
export interface IFleetCutoverIngressPreparationRequest {
|
|
521
|
+
mutationId: string;
|
|
522
|
+
clusterId: string;
|
|
523
|
+
ingressServiceId: string;
|
|
524
|
+
currentRuntime: IClusterRuntimeReference & { phase: 'pallet' };
|
|
525
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
526
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
527
|
+
expectedSpecRevision: number;
|
|
528
|
+
/** Null for creation; the exact current designation for same-service credential rotation. */
|
|
529
|
+
expectedDesignation: IFleetCutoverAcceptedEvidenceReference | null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Public identity of the active ingress credential generation; it contains no credential value. */
|
|
533
|
+
export interface IFleetCutoverIngressCredentialReference {
|
|
534
|
+
credentialId: string;
|
|
535
|
+
generation: number;
|
|
536
|
+
secretVersion: ISecretVersionReference;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Cloudly's accepted pre-assignment designation. This grants no assignment, session,
|
|
541
|
+
* registration, publication, port-transfer, or post-registration ingress authority.
|
|
542
|
+
*/
|
|
543
|
+
export interface IFleetCutoverIngressDesignation {
|
|
544
|
+
id: string;
|
|
545
|
+
generation: number;
|
|
546
|
+
previous: IFleetCutoverAcceptedEvidenceReference | null;
|
|
547
|
+
clusterId: string;
|
|
548
|
+
runtime: IClusterRuntimeReference & { phase: 'pallet' };
|
|
549
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
550
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
551
|
+
ingressServiceId: string;
|
|
552
|
+
preparedSpecRevision: number;
|
|
553
|
+
credential: IFleetCutoverIngressCredentialReference;
|
|
554
|
+
actorId: string;
|
|
555
|
+
acceptedAt: number;
|
|
556
|
+
digest: TSha256Digest;
|
|
557
|
+
}
|
|
558
|
+
|
|
376
559
|
export interface IFleetCutoverIngressAuthority {
|
|
377
560
|
id: string;
|
|
378
561
|
generation: number;
|
|
@@ -453,6 +636,11 @@ export const fleetCutoverContract = Object.freeze({
|
|
|
453
636
|
maximumNodes: 1024,
|
|
454
637
|
maximumContainersPerNode: 512,
|
|
455
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,
|
|
456
644
|
maximumClassifications: 512,
|
|
457
645
|
observationFreshnessMs: 300_000,
|
|
458
646
|
mountSourceIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-source-identity',
|
|
@@ -464,9 +652,18 @@ export const fleetCutoverContract = Object.freeze({
|
|
|
464
652
|
censusDigestDomain: 'serve.zone/fleet-cutover-census',
|
|
465
653
|
scopeDigestDomain: 'serve.zone/fleet-cutover-scope',
|
|
466
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',
|
|
467
662
|
volumeFenceDigestDomain: 'serve.zone/fleet-cutover-volume-fence',
|
|
468
663
|
corestoreAdoptionDigestDomain: 'serve.zone/fleet-cutover-corestore-adoption',
|
|
664
|
+
storageAdoptionGrantDigestDomain: 'serve.zone/fleet-cutover-storage-adoption-grant',
|
|
469
665
|
coreflowRetirementDigestDomain: 'serve.zone/fleet-cutover-coreflow-retirement',
|
|
666
|
+
ingressDesignationDigestDomain: 'serve.zone/fleet-cutover-ingress-designation',
|
|
470
667
|
ingressAuthorityDigestDomain: 'serve.zone/fleet-cutover-ingress-authority',
|
|
471
668
|
legalPhaseTransitions: Object.freeze({
|
|
472
669
|
swarm: 'draining',
|
|
@@ -518,6 +715,20 @@ const assignmentReference = (valueArg: unknown): IRuntimeAssignmentReference =>
|
|
|
518
715
|
digest: networkDigest(value.digest),
|
|
519
716
|
};
|
|
520
717
|
};
|
|
718
|
+
const ingressCredentialReference = (
|
|
719
|
+
valueArg: unknown,
|
|
720
|
+
): IFleetCutoverIngressCredentialReference => {
|
|
721
|
+
const value = networkRecord(valueArg, ['credentialId', 'generation', 'secretVersion']);
|
|
722
|
+
const secretVersion = networkRecord(value.secretVersion, ['secretId', 'secretVersionId']);
|
|
723
|
+
return {
|
|
724
|
+
credentialId: networkId(value.credentialId),
|
|
725
|
+
generation: networkCounter(value.generation),
|
|
726
|
+
secretVersion: {
|
|
727
|
+
secretId: networkId(secretVersion.secretId),
|
|
728
|
+
secretVersionId: networkId(secretVersion.secretVersionId),
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
};
|
|
521
732
|
const mountTypeAndClassification = (
|
|
522
733
|
typeArg: unknown,
|
|
523
734
|
classificationArg: unknown,
|
|
@@ -994,6 +1205,13 @@ export const fleetCutoverEvidenceReferenceOf = (
|
|
|
994
1205
|
digest: valueArg.digest,
|
|
995
1206
|
});
|
|
996
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
|
+
|
|
997
1215
|
export const snapshotFleetCutoverAcceptedEvidenceReference = (
|
|
998
1216
|
valueArg: unknown,
|
|
999
1217
|
): IFleetCutoverAcceptedEvidenceReference => safe(() => reference(captureNetwork(valueArg)));
|
|
@@ -1150,6 +1368,383 @@ export const computeFleetCutoverVolumeHandoffDigest = async (
|
|
|
1150
1368
|
snapshotFleetCutoverVolumeHandoffIntent(valueArg),
|
|
1151
1369
|
);
|
|
1152
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
|
+
|
|
1153
1748
|
const volumeFence = (valueArg: unknown): TFleetCutoverVolumeFence => {
|
|
1154
1749
|
requireNetwork(valueArg !== null && typeof valueArg === 'object');
|
|
1155
1750
|
const state = (valueArg as Record<string, unknown>).state;
|
|
@@ -1265,6 +1860,45 @@ export const bindFleetCutoverAdoptionToVolumeFence = (
|
|
|
1265
1860
|
return { adoption, fence };
|
|
1266
1861
|
});
|
|
1267
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
|
+
|
|
1268
1902
|
export const validateFleetCutoverVolumeHandoffIntent = async (valueArg: unknown): Promise<string[]> => {
|
|
1269
1903
|
try {
|
|
1270
1904
|
const value = snapshotFleetCutoverVolumeHandoffIntent(valueArg);
|
|
@@ -1314,6 +1948,273 @@ const runtimeSession = (valueArg: unknown): IRuntimeSessionBinding => {
|
|
|
1314
1948
|
};
|
|
1315
1949
|
};
|
|
1316
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
|
+
|
|
1317
2218
|
export const snapshotFleetCutoverCoreflowRetirementEvidence = (
|
|
1318
2219
|
valueArg: unknown,
|
|
1319
2220
|
): IFleetCutoverCoreflowRetirementEvidence => safe(() => {
|
|
@@ -1418,6 +2319,83 @@ export const validateFleetCutoverCoreflowRetirementEvidence = async (
|
|
|
1418
2319
|
} catch { return ['fleet cutover Coreflow retirement evidence must use its exact canonical schema']; }
|
|
1419
2320
|
};
|
|
1420
2321
|
|
|
2322
|
+
export const snapshotFleetCutoverIngressPreparationRequest = (
|
|
2323
|
+
valueArg: unknown,
|
|
2324
|
+
): IFleetCutoverIngressPreparationRequest => safe(() => {
|
|
2325
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
2326
|
+
'mutationId', 'clusterId', 'ingressServiceId', 'currentRuntime', 'scope', 'targetSet',
|
|
2327
|
+
'expectedSpecRevision', 'expectedDesignation',
|
|
2328
|
+
]);
|
|
2329
|
+
const clusterId = networkId(value.clusterId);
|
|
2330
|
+
const currentRuntime = runtimeReference(value.currentRuntime);
|
|
2331
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
2332
|
+
requireNetwork(currentRuntime.id === clusterId && currentRuntime.phase === 'pallet'
|
|
2333
|
+
&& targetSet.cloudlyClusterId === clusterId);
|
|
2334
|
+
return {
|
|
2335
|
+
mutationId: networkId(value.mutationId),
|
|
2336
|
+
clusterId,
|
|
2337
|
+
ingressServiceId: networkId(value.ingressServiceId),
|
|
2338
|
+
currentRuntime: currentRuntime as IFleetCutoverIngressPreparationRequest['currentRuntime'],
|
|
2339
|
+
scope: reference(value.scope),
|
|
2340
|
+
targetSet,
|
|
2341
|
+
expectedSpecRevision: networkCounter(value.expectedSpecRevision),
|
|
2342
|
+
expectedDesignation: value.expectedDesignation === null
|
|
2343
|
+
? null : reference(value.expectedDesignation),
|
|
2344
|
+
};
|
|
2345
|
+
});
|
|
2346
|
+
|
|
2347
|
+
export const snapshotFleetCutoverIngressDesignation = (
|
|
2348
|
+
valueArg: unknown,
|
|
2349
|
+
): IFleetCutoverIngressDesignation => safe(() => {
|
|
2350
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
2351
|
+
'id', 'generation', 'previous', 'clusterId', 'runtime', 'scope', 'targetSet',
|
|
2352
|
+
'ingressServiceId', 'preparedSpecRevision', 'credential', 'actorId', 'acceptedAt', 'digest',
|
|
2353
|
+
]);
|
|
2354
|
+
const id = networkId(value.id);
|
|
2355
|
+
const generation = networkCounter(value.generation);
|
|
2356
|
+
const previous = value.previous === null ? null : reference(value.previous);
|
|
2357
|
+
const clusterId = networkId(value.clusterId);
|
|
2358
|
+
const runtime = runtimeReference(value.runtime);
|
|
2359
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
2360
|
+
requireNetwork(runtime.id === clusterId && runtime.phase === 'pallet'
|
|
2361
|
+
&& targetSet.cloudlyClusterId === clusterId
|
|
2362
|
+
&& (generation === 1
|
|
2363
|
+
? previous === null
|
|
2364
|
+
: previous?.id === id && previous.generation === generation - 1));
|
|
2365
|
+
return {
|
|
2366
|
+
id,
|
|
2367
|
+
generation,
|
|
2368
|
+
previous,
|
|
2369
|
+
clusterId,
|
|
2370
|
+
runtime: runtime as IFleetCutoverIngressDesignation['runtime'],
|
|
2371
|
+
scope: reference(value.scope),
|
|
2372
|
+
targetSet,
|
|
2373
|
+
ingressServiceId: networkId(value.ingressServiceId),
|
|
2374
|
+
preparedSpecRevision: networkCounter(value.preparedSpecRevision),
|
|
2375
|
+
credential: ingressCredentialReference(value.credential),
|
|
2376
|
+
actorId: networkId(value.actorId),
|
|
2377
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
2378
|
+
digest: networkDigest(value.digest),
|
|
2379
|
+
};
|
|
2380
|
+
});
|
|
2381
|
+
|
|
2382
|
+
export const computeFleetCutoverIngressDesignationDigest = async (
|
|
2383
|
+
valueArg: unknown,
|
|
2384
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
2385
|
+
fleetCutoverContract.ingressDesignationDigestDomain,
|
|
2386
|
+
snapshotFleetCutoverIngressDesignation(valueArg),
|
|
2387
|
+
);
|
|
2388
|
+
|
|
2389
|
+
export const validateFleetCutoverIngressDesignation = async (
|
|
2390
|
+
valueArg: unknown,
|
|
2391
|
+
): Promise<string[]> => {
|
|
2392
|
+
try {
|
|
2393
|
+
const value = snapshotFleetCutoverIngressDesignation(valueArg);
|
|
2394
|
+
return value.digest === await computeFleetCutoverIngressDesignationDigest(value)
|
|
2395
|
+
? [] : ['fleet cutover ingress designation digest does not match'];
|
|
2396
|
+
} catch { return ['fleet cutover ingress designation must use its exact canonical schema']; }
|
|
2397
|
+
};
|
|
2398
|
+
|
|
1421
2399
|
export const snapshotFleetCutoverIngressAuthority = (
|
|
1422
2400
|
valueArg: unknown,
|
|
1423
2401
|
): IFleetCutoverIngressAuthority => safe(() => {
|