@serve.zone/interfaces 32.14.0 → 32.16.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 +27 -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 +263 -0
- package/dist_ts/data/fleetcutover.js +784 -7
- 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 +118 -16
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/clusternode.ts +209 -2
- package/ts/data/fleetcutover.ts +1156 -5
- 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,9 +6,20 @@ 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,
|
|
22
|
+
compareWireTokens,
|
|
12
23
|
createCanonicalJsonSha256Hex,
|
|
13
24
|
} from '../private/canonicaljson.js';
|
|
14
25
|
import {
|
|
@@ -72,6 +83,37 @@ export interface IFleetCutoverMountTargetIdentityInput {
|
|
|
72
83
|
target: string;
|
|
73
84
|
}
|
|
74
85
|
|
|
86
|
+
export interface IFleetCutoverDockerMountInput {
|
|
87
|
+
/** Raw Docker mount type. Unsupported values produce an explicit unknown descriptor. */
|
|
88
|
+
type: string;
|
|
89
|
+
/** Exact source bytes from the Docker object being described; never substitute another field. */
|
|
90
|
+
source: string;
|
|
91
|
+
/** Exact container target bytes from the Docker object being described. */
|
|
92
|
+
target: string;
|
|
93
|
+
readOnly: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Value-free correlation between one local physical mount and its service-spec mount identity.
|
|
98
|
+
* The two identities stay distinct: a Docker volume's local Source is a host path while its
|
|
99
|
+
* service Source and local Name are the logical volume name.
|
|
100
|
+
*/
|
|
101
|
+
export interface IFleetCutoverPhysicalMountCorrelation {
|
|
102
|
+
physicalMountIdentityDigest: TSha256Digest;
|
|
103
|
+
serviceMountIdentityDigest: TSha256Digest;
|
|
104
|
+
correlationIdentityDigest: TSha256Digest;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface IFleetCutoverPhysicalMountEvidence {
|
|
108
|
+
mount: IFleetCutoverMountDescriptor;
|
|
109
|
+
correlation: IFleetCutoverPhysicalMountCorrelation;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface IFleetCutoverDockerConstraintSetIdentity {
|
|
113
|
+
constraintCount: number;
|
|
114
|
+
constraintSetDigest: TSha256Digest;
|
|
115
|
+
}
|
|
116
|
+
|
|
75
117
|
export type TFleetCutoverDockerTaskState =
|
|
76
118
|
| 'new'
|
|
77
119
|
| 'allocated'
|
|
@@ -138,6 +180,11 @@ export interface IFleetCutoverPhysicalContainer {
|
|
|
138
180
|
imageId: TSha256Digest;
|
|
139
181
|
serviceIdLabel: string | null;
|
|
140
182
|
mounts: IFleetCutoverMountDescriptor[];
|
|
183
|
+
/**
|
|
184
|
+
* Optional only for compatibility with observations predating correlation evidence. When
|
|
185
|
+
* present it is a complete one-to-one mapping of mounts. Absence establishes no mount join.
|
|
186
|
+
*/
|
|
187
|
+
mountCorrelations?: IFleetCutoverPhysicalMountCorrelation[];
|
|
141
188
|
}
|
|
142
189
|
|
|
143
190
|
export interface IFleetCutoverContainerSetIdentity {
|
|
@@ -281,6 +328,101 @@ export interface IFleetCutoverVolumeHandoffIntent {
|
|
|
281
328
|
digest: TSha256Digest;
|
|
282
329
|
}
|
|
283
330
|
|
|
331
|
+
export interface IFleetCutoverPersistentVolumeMountDescriptor
|
|
332
|
+
extends IFleetCutoverMountDescriptor {
|
|
333
|
+
type: 'volume';
|
|
334
|
+
classification: 'persistent-volume';
|
|
335
|
+
readOnly: false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Cloudly-owned desired handoff prepared from authenticated legacy observations. Observed Docker
|
|
340
|
+
* source identity is not physical storage lineage; Corestore proves lineage later under the held
|
|
341
|
+
* native attachment fence.
|
|
342
|
+
*/
|
|
343
|
+
export interface IFleetCutoverPreparedVolumeHandoffIntent {
|
|
344
|
+
id: string;
|
|
345
|
+
generation: number;
|
|
346
|
+
clusterId: string;
|
|
347
|
+
nodeId: string;
|
|
348
|
+
serviceId: string;
|
|
349
|
+
volumeId: string;
|
|
350
|
+
observedMount: IFleetCutoverPersistentVolumeMountDescriptor;
|
|
351
|
+
claimIntent: IRuntimeStorageClaimIntent;
|
|
352
|
+
runtime: IClusterRuntimeReference & { phase: 'draining' };
|
|
353
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
354
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
355
|
+
legacyStorageEvidence: IFleetCutoverAcceptedEvidenceReference;
|
|
356
|
+
sourceNodeObservation: IFleetCutoverAcceptedEvidenceReference;
|
|
357
|
+
acceptedAt: number;
|
|
358
|
+
digest: TSha256Digest;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export interface IFleetCutoverPreparedVolumeHandoffUpsert {
|
|
362
|
+
nodeId: string;
|
|
363
|
+
serviceId: string;
|
|
364
|
+
mountIdentityDigest: TSha256Digest;
|
|
365
|
+
request: IAppStoreFilesystemStorageRequest & {
|
|
366
|
+
accessMode: 'ReadWriteOnce';
|
|
367
|
+
reclaimPolicy: 'retain';
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** One bounded CAS mutation; the durable aggregate may contain many such 32-entry batches. */
|
|
372
|
+
export interface IFleetCutoverPreparedVolumeHandoffMutation {
|
|
373
|
+
mutationId: string;
|
|
374
|
+
clusterId: string;
|
|
375
|
+
currentRuntime: IClusterRuntimeReference & { phase: 'draining' };
|
|
376
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
377
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
378
|
+
expectedAggregate: IFleetCutoverAcceptedEvidenceReference | null;
|
|
379
|
+
upserts: IFleetCutoverPreparedVolumeHandoffUpsert[];
|
|
380
|
+
removals: IFleetCutoverAcceptedEvidenceReference[];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Revisioned header for one complete expected set. It intentionally contains no page references:
|
|
385
|
+
* immutable pages bind back to this exact revision, avoiding an aggregate/page digest cycle.
|
|
386
|
+
*/
|
|
387
|
+
export interface IFleetCutoverPreparedVolumeHandoffAggregate {
|
|
388
|
+
id: string;
|
|
389
|
+
generation: number;
|
|
390
|
+
previous: IFleetCutoverAcceptedEvidenceReference | null;
|
|
391
|
+
clusterId: string;
|
|
392
|
+
runtime: IClusterRuntimeReference & { phase: 'draining' };
|
|
393
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
394
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
395
|
+
state: 'incomplete' | 'complete';
|
|
396
|
+
requiredHandoffCount: number;
|
|
397
|
+
handoffCount: number;
|
|
398
|
+
pageCount: number;
|
|
399
|
+
handoffSetDigest: TSha256Digest;
|
|
400
|
+
acceptedAt: number;
|
|
401
|
+
digest: TSha256Digest;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** One immutable 1..32-entry page pinned to one aggregate revision. */
|
|
405
|
+
export interface IFleetCutoverPreparedVolumeHandoffPage {
|
|
406
|
+
id: string;
|
|
407
|
+
generation: 1;
|
|
408
|
+
aggregate: IFleetCutoverAcceptedEvidenceReference;
|
|
409
|
+
pageIndex: number;
|
|
410
|
+
handoffs: IFleetCutoverPreparedVolumeHandoffPageReference[];
|
|
411
|
+
handoffSetPageDigest: TSha256Digest;
|
|
412
|
+
digest: TSha256Digest;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Immutable page membership commits both the handoff revision and its owning physical node. */
|
|
416
|
+
export interface IFleetCutoverPreparedVolumeHandoffPageReference
|
|
417
|
+
extends IFleetCutoverAcceptedEvidenceReference {
|
|
418
|
+
nodeId: string;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export interface IFleetCutoverPreparedVolumeHandoffPageSetEntry {
|
|
422
|
+
pageIndex: number;
|
|
423
|
+
handoffSetPageDigest: TSha256Digest;
|
|
424
|
+
}
|
|
425
|
+
|
|
284
426
|
interface IFleetCutoverVolumeFenceBase {
|
|
285
427
|
id: string;
|
|
286
428
|
generation: number;
|
|
@@ -328,6 +470,40 @@ export interface IFleetCutoverCorestoreAdoptionReceipt {
|
|
|
328
470
|
digest: TSha256Digest;
|
|
329
471
|
}
|
|
330
472
|
|
|
473
|
+
/** Current-session Cloudly grant for one post-Pallet adoption attempt. */
|
|
474
|
+
export interface IFleetCutoverStorageAdoptionGrant {
|
|
475
|
+
id: string;
|
|
476
|
+
generation: number;
|
|
477
|
+
previous: IFleetCutoverAcceptedEvidenceReference | null;
|
|
478
|
+
clusterId: string;
|
|
479
|
+
nodeId: string;
|
|
480
|
+
serviceId: string;
|
|
481
|
+
volumeId: string;
|
|
482
|
+
observedMount: IFleetCutoverPersistentVolumeMountDescriptor;
|
|
483
|
+
runtime: IClusterRuntimeReference & { phase: 'pallet' };
|
|
484
|
+
session: IRuntimeSessionBinding;
|
|
485
|
+
handoffIntent: IFleetCutoverAcceptedEvidenceReference;
|
|
486
|
+
palletFence: IFleetCutoverAcceptedEvidenceReference;
|
|
487
|
+
acceptedAt: number;
|
|
488
|
+
digest: TSha256Digest;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Exact result Pallet persists only after revalidating the current grant following adoption. */
|
|
492
|
+
export interface IFleetCutoverStorageAdoptionBundle {
|
|
493
|
+
grant: IFleetCutoverStorageAdoptionGrant;
|
|
494
|
+
fence: Extract<TFleetCutoverVolumeFence, { state: 'consumed' }>;
|
|
495
|
+
adoption: IFleetCutoverCorestoreAdoptionReceipt;
|
|
496
|
+
claim: IRuntimeStorageClaim;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** One authenticated Spark reporter's complete bounded subset of a full immutable aggregate page. */
|
|
500
|
+
export interface IFleetCutoverHeldVolumeFenceEvidencePage {
|
|
501
|
+
aggregate: IFleetCutoverAcceptedEvidenceReference;
|
|
502
|
+
page: IFleetCutoverPreparedVolumeHandoffPage;
|
|
503
|
+
nodeId: string;
|
|
504
|
+
fences: Extract<TFleetCutoverVolumeFence, { state: 'held' }>[];
|
|
505
|
+
}
|
|
506
|
+
|
|
331
507
|
export interface IFleetCutoverCoreflowRetirementEvidence {
|
|
332
508
|
id: string;
|
|
333
509
|
generation: number;
|
|
@@ -497,19 +673,38 @@ export const fleetCutoverContract = Object.freeze({
|
|
|
497
673
|
maximumNodes: 1024,
|
|
498
674
|
maximumContainersPerNode: 512,
|
|
499
675
|
maximumMountsPerOwner: 32,
|
|
676
|
+
maximumConstraintsPerService: 256,
|
|
677
|
+
maximumDockerTextBytes: 4096,
|
|
678
|
+
maximumDockerServiceSpecCanonicalBytes: 1_048_576,
|
|
679
|
+
maximumPreparedVolumeHandoffMutationEntries: 32,
|
|
680
|
+
/** Every admitted task has one node placement and at most 32 mounts: 4096 * 32. */
|
|
681
|
+
maximumPreparedVolumeHandoffs: 4096 * 32,
|
|
682
|
+
maximumPreparedVolumeHandoffPages: (4096 * 32) / 32,
|
|
683
|
+
maximumHeldVolumeFenceEvidenceBytes: 131_072,
|
|
500
684
|
maximumClassifications: 512,
|
|
501
685
|
observationFreshnessMs: 300_000,
|
|
502
686
|
mountSourceIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-source-identity',
|
|
503
687
|
mountTargetIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-target-identity',
|
|
504
688
|
mountIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-identity',
|
|
689
|
+
mountCorrelationIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-correlation-identity',
|
|
690
|
+
dockerServiceSpecDigestDomain: 'serve.zone/fleet-cutover-docker-service-spec',
|
|
691
|
+
dockerConstraintSetDigestDomain: 'serve.zone/fleet-cutover-docker-constraint-set',
|
|
505
692
|
containerSetIdentityDigestDomain: 'serve.zone/fleet-cutover-container-set-identity',
|
|
506
693
|
localNodeSnapshotDigestDomain: 'serve.zone/fleet-cutover-local-node-snapshot',
|
|
507
694
|
managerSnapshotDigestDomain: 'serve.zone/fleet-cutover-manager-snapshot',
|
|
508
695
|
censusDigestDomain: 'serve.zone/fleet-cutover-census',
|
|
509
696
|
scopeDigestDomain: 'serve.zone/fleet-cutover-scope',
|
|
510
697
|
volumeHandoffDigestDomain: 'serve.zone/fleet-cutover-volume-handoff',
|
|
698
|
+
preparedVolumeHandoffDigestDomain: 'serve.zone/fleet-cutover-prepared-volume-handoff',
|
|
699
|
+
preparedVolumeHandoffAggregateDigestDomain:
|
|
700
|
+
'serve.zone/fleet-cutover-prepared-volume-handoff-aggregate',
|
|
701
|
+
preparedVolumeHandoffPageContentDigestDomain:
|
|
702
|
+
'serve.zone/fleet-cutover-prepared-volume-handoff-page-content',
|
|
703
|
+
preparedVolumeHandoffPageDigestDomain: 'serve.zone/fleet-cutover-prepared-volume-handoff-page',
|
|
704
|
+
preparedVolumeHandoffSetDigestDomain: 'serve.zone/fleet-cutover-prepared-volume-handoff-set',
|
|
511
705
|
volumeFenceDigestDomain: 'serve.zone/fleet-cutover-volume-fence',
|
|
512
706
|
corestoreAdoptionDigestDomain: 'serve.zone/fleet-cutover-corestore-adoption',
|
|
707
|
+
storageAdoptionGrantDigestDomain: 'serve.zone/fleet-cutover-storage-adoption-grant',
|
|
513
708
|
coreflowRetirementDigestDomain: 'serve.zone/fleet-cutover-coreflow-retirement',
|
|
514
709
|
ingressDesignationDigestDomain: 'serve.zone/fleet-cutover-ingress-designation',
|
|
515
710
|
ingressAuthorityDigestDomain: 'serve.zone/fleet-cutover-ingress-authority',
|
|
@@ -521,6 +716,11 @@ export const fleetCutoverContract = Object.freeze({
|
|
|
521
716
|
} satisfies Record<TClusterRuntimePhase, TClusterRuntimePhase | null>),
|
|
522
717
|
} as const);
|
|
523
718
|
|
|
719
|
+
/** Docker labels with established historical producer semantics. */
|
|
720
|
+
export const fleetCutoverDockerLabelNames = Object.freeze({
|
|
721
|
+
serviceId: 'serve.zone.serviceId',
|
|
722
|
+
} as const);
|
|
723
|
+
|
|
524
724
|
const fail = (): never => { throw new Error('Fleet cutover contract rejected.'); };
|
|
525
725
|
const safe = <T>(operationArg: () => T): T => {
|
|
526
726
|
try { return operationArg(); } catch { return fail(); }
|
|
@@ -612,6 +812,28 @@ const sortedMountDescriptors = (valueArg: unknown): IFleetCutoverMountDescriptor
|
|
|
612
812
|
networkList(valueArg, fleetCutoverContract.maximumMountsPerOwner, mountDescriptor),
|
|
613
813
|
itemArg => itemArg.mountIdentityDigest,
|
|
614
814
|
);
|
|
815
|
+
const physicalMountCorrelation = (
|
|
816
|
+
valueArg: unknown,
|
|
817
|
+
): IFleetCutoverPhysicalMountCorrelation => {
|
|
818
|
+
const value = networkRecord(valueArg, [
|
|
819
|
+
'physicalMountIdentityDigest', 'serviceMountIdentityDigest', 'correlationIdentityDigest',
|
|
820
|
+
]);
|
|
821
|
+
return {
|
|
822
|
+
physicalMountIdentityDigest: networkDigest(value.physicalMountIdentityDigest),
|
|
823
|
+
serviceMountIdentityDigest: networkDigest(value.serviceMountIdentityDigest),
|
|
824
|
+
correlationIdentityDigest: networkDigest(value.correlationIdentityDigest),
|
|
825
|
+
};
|
|
826
|
+
};
|
|
827
|
+
const sortedPhysicalMountCorrelations = (
|
|
828
|
+
valueArg: unknown,
|
|
829
|
+
): IFleetCutoverPhysicalMountCorrelation[] => networkSorted(
|
|
830
|
+
networkList(
|
|
831
|
+
valueArg,
|
|
832
|
+
fleetCutoverContract.maximumMountsPerOwner,
|
|
833
|
+
physicalMountCorrelation,
|
|
834
|
+
),
|
|
835
|
+
itemArg => itemArg.physicalMountIdentityDigest,
|
|
836
|
+
);
|
|
615
837
|
const dockerMode = (valueArg: unknown): TFleetCutoverDockerServiceMode => {
|
|
616
838
|
requireNetwork(valueArg !== null && typeof valueArg === 'object');
|
|
617
839
|
const kind = (valueArg as Record<string, unknown>).kind;
|
|
@@ -636,6 +858,8 @@ const dockerService = (valueArg: unknown): IFleetCutoverDockerService => {
|
|
|
636
858
|
'dockerServiceId', 'dockerServiceName', 'specVersion', 'specDigest', 'imageDigest', 'mode',
|
|
637
859
|
'desiredTaskCount', 'constraintCount', 'constraintSetDigest', 'mounts', 'ownershipLabels',
|
|
638
860
|
]);
|
|
861
|
+
const constraintCount = nonNegative(value.constraintCount);
|
|
862
|
+
requireNetwork(constraintCount <= fleetCutoverContract.maximumConstraintsPerService);
|
|
639
863
|
return {
|
|
640
864
|
dockerServiceId: networkId(value.dockerServiceId),
|
|
641
865
|
dockerServiceName: networkId(value.dockerServiceName),
|
|
@@ -644,7 +868,7 @@ const dockerService = (valueArg: unknown): IFleetCutoverDockerService => {
|
|
|
644
868
|
imageDigest: networkDigest(value.imageDigest),
|
|
645
869
|
mode: dockerMode(value.mode),
|
|
646
870
|
desiredTaskCount: nonNegative(value.desiredTaskCount),
|
|
647
|
-
constraintCount
|
|
871
|
+
constraintCount,
|
|
648
872
|
constraintSetDigest: networkDigest(value.constraintSetDigest),
|
|
649
873
|
mounts: sortedMountDescriptors(value.mounts),
|
|
650
874
|
ownershipLabels: ownershipLabels(value.ownershipLabels),
|
|
@@ -681,8 +905,8 @@ const dockerTask = (valueArg: unknown): IFleetCutoverDockerTask => {
|
|
|
681
905
|
const physicalContainer = (valueArg: unknown): IFleetCutoverPhysicalContainer => {
|
|
682
906
|
const value = networkRecord(valueArg, [
|
|
683
907
|
'dockerContainerId', 'dockerServiceId', 'dockerTaskId', 'imageId', 'serviceIdLabel', 'mounts',
|
|
684
|
-
]);
|
|
685
|
-
|
|
908
|
+
], ['mountCorrelations']);
|
|
909
|
+
const result: IFleetCutoverPhysicalContainer = {
|
|
686
910
|
dockerContainerId: networkId(value.dockerContainerId),
|
|
687
911
|
dockerServiceId: value.dockerServiceId === null ? null : networkId(value.dockerServiceId),
|
|
688
912
|
dockerTaskId: value.dockerTaskId === null ? null : networkId(value.dockerTaskId),
|
|
@@ -690,6 +914,10 @@ const physicalContainer = (valueArg: unknown): IFleetCutoverPhysicalContainer =>
|
|
|
690
914
|
serviceIdLabel: value.serviceIdLabel === null ? null : networkId(value.serviceIdLabel),
|
|
691
915
|
mounts: sortedMountDescriptors(value.mounts),
|
|
692
916
|
};
|
|
917
|
+
if (Object.hasOwn(value, 'mountCorrelations')) {
|
|
918
|
+
result.mountCorrelations = sortedPhysicalMountCorrelations(value.mountCorrelations);
|
|
919
|
+
}
|
|
920
|
+
return result;
|
|
693
921
|
};
|
|
694
922
|
const inventory = (valueArg: unknown): IFleetCutoverDockerInventory => {
|
|
695
923
|
const value = networkRecord(valueArg, [
|
|
@@ -773,11 +1001,17 @@ const canonicalDigest = async (domainArg: string, valueArg: object): Promise<TSh
|
|
|
773
1001
|
|
|
774
1002
|
const exactDockerText = (valueArg: unknown, allowEmptyArg: boolean): string => {
|
|
775
1003
|
requireNetwork(typeof valueArg === 'string'
|
|
776
|
-
&& new TextEncoder().encode(valueArg).length <=
|
|
1004
|
+
&& new TextEncoder().encode(valueArg).length <= fleetCutoverContract.maximumDockerTextBytes
|
|
777
1005
|
&& (allowEmptyArg || valueArg.length > 0));
|
|
778
1006
|
return valueArg;
|
|
779
1007
|
};
|
|
780
1008
|
|
|
1009
|
+
const openDockerRecord = (valueArg: unknown): Record<string, unknown> => {
|
|
1010
|
+
const value = captureNetwork(valueArg);
|
|
1011
|
+
requireNetwork(value !== null && typeof value === 'object' && !Array.isArray(value));
|
|
1012
|
+
return value as Record<string, unknown>;
|
|
1013
|
+
};
|
|
1014
|
+
|
|
781
1015
|
const computeExactIdentityDigest = async (
|
|
782
1016
|
domainArg: string,
|
|
783
1017
|
inputArg: object,
|
|
@@ -786,6 +1020,160 @@ const computeExactIdentityDigest = async (
|
|
|
786
1020
|
input: inputArg,
|
|
787
1021
|
}, fail), fail)}`;
|
|
788
1022
|
|
|
1023
|
+
/**
|
|
1024
|
+
* Hash the complete open-schema Docker Service.Spec object. Unknown response properties are
|
|
1025
|
+
* intentionally included: this helper returns only the digest and never a retained projection.
|
|
1026
|
+
*/
|
|
1027
|
+
export const computeFleetCutoverDockerServiceSpecDigest = async (
|
|
1028
|
+
valueArg: unknown,
|
|
1029
|
+
): Promise<TSha256Digest> => {
|
|
1030
|
+
try {
|
|
1031
|
+
const spec = openDockerRecord(valueArg);
|
|
1032
|
+
const canonicalSpec = canonicalizeStrictJson(spec, fail, 'Docker Service.Spec');
|
|
1033
|
+
requireNetwork(new TextEncoder().encode(canonicalSpec).byteLength
|
|
1034
|
+
<= fleetCutoverContract.maximumDockerServiceSpecCanonicalBytes);
|
|
1035
|
+
return await computeExactIdentityDigest(
|
|
1036
|
+
fleetCutoverContract.dockerServiceSpecDigestDomain,
|
|
1037
|
+
{ spec },
|
|
1038
|
+
);
|
|
1039
|
+
} catch {
|
|
1040
|
+
return fail();
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
/** Sort exact constraint strings while preserving duplicate entries, then return value-free identity. */
|
|
1045
|
+
export const computeFleetCutoverDockerConstraintSetIdentity = async (
|
|
1046
|
+
valueArg: unknown,
|
|
1047
|
+
): Promise<IFleetCutoverDockerConstraintSetIdentity> => {
|
|
1048
|
+
try {
|
|
1049
|
+
const captured = captureNetwork(valueArg);
|
|
1050
|
+
requireNetwork(Array.isArray(captured)
|
|
1051
|
+
&& captured.length <= fleetCutoverContract.maximumConstraintsPerService);
|
|
1052
|
+
const constraints = captured.map(itemArg => exactDockerText(itemArg, false))
|
|
1053
|
+
.sort(compareWireTokens);
|
|
1054
|
+
return {
|
|
1055
|
+
constraintCount: constraints.length,
|
|
1056
|
+
constraintSetDigest: await computeExactIdentityDigest(
|
|
1057
|
+
fleetCutoverContract.dockerConstraintSetDigestDomain,
|
|
1058
|
+
{ constraints },
|
|
1059
|
+
),
|
|
1060
|
+
};
|
|
1061
|
+
} catch {
|
|
1062
|
+
return fail();
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
export const snapshotFleetCutoverDockerDesiredTaskCount = (valueArg: unknown): number =>
|
|
1067
|
+
safe(() => nonNegative(valueArg));
|
|
1068
|
+
|
|
1069
|
+
export const validateFleetCutoverDockerDesiredTaskCount = (valueArg: unknown): string[] => (
|
|
1070
|
+
typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg >= 0
|
|
1071
|
+
? []
|
|
1072
|
+
: ['Docker ServiceStatus.DesiredTasks must be a non-negative safe integer']
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
/** Apply only the documented exact cutover mount classifications; no path normalization occurs. */
|
|
1076
|
+
export const classifyFleetCutoverDockerMount = (
|
|
1077
|
+
valueArg: IFleetCutoverDockerMountInput,
|
|
1078
|
+
): Pick<IFleetCutoverMountDescriptor, 'type' | 'classification'> => safe(() => {
|
|
1079
|
+
const value = networkRecord(valueArg, ['type', 'source', 'target', 'readOnly']);
|
|
1080
|
+
const rawType = exactDockerText(value.type, false);
|
|
1081
|
+
const source = exactDockerText(value.source, true);
|
|
1082
|
+
const target = exactDockerText(value.target, false);
|
|
1083
|
+
requireNetwork(typeof value.readOnly === 'boolean');
|
|
1084
|
+
if (rawType === 'volume') return { type: 'volume', classification: 'persistent-volume' };
|
|
1085
|
+
if (rawType === 'tmpfs') return { type: 'tmpfs', classification: 'tmpfs' };
|
|
1086
|
+
if (rawType !== 'bind') return { type: 'unknown', classification: 'unknown' };
|
|
1087
|
+
if (source === '/var/run/docker.sock'
|
|
1088
|
+
&& target === '/var/run/docker.sock'
|
|
1089
|
+
&& value.readOnly === false) {
|
|
1090
|
+
return { type: 'bind', classification: 'docker-socket' };
|
|
1091
|
+
}
|
|
1092
|
+
if (source === '/sys/fs/cgroup'
|
|
1093
|
+
&& target === '/host/cgroup'
|
|
1094
|
+
&& value.readOnly === true) {
|
|
1095
|
+
return { type: 'bind', classification: 'cgroup' };
|
|
1096
|
+
}
|
|
1097
|
+
return { type: 'bind', classification: 'bind' };
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
export const createFleetCutoverMountDescriptor = async (
|
|
1101
|
+
valueArg: IFleetCutoverDockerMountInput,
|
|
1102
|
+
): Promise<IFleetCutoverMountDescriptor> => {
|
|
1103
|
+
try {
|
|
1104
|
+
const value = networkRecord(valueArg, ['type', 'source', 'target', 'readOnly']);
|
|
1105
|
+
const source = exactDockerText(value.source, true);
|
|
1106
|
+
const target = exactDockerText(value.target, false);
|
|
1107
|
+
requireNetwork(typeof value.readOnly === 'boolean');
|
|
1108
|
+
const shape = classifyFleetCutoverDockerMount({
|
|
1109
|
+
type: exactDockerText(value.type, false),
|
|
1110
|
+
source,
|
|
1111
|
+
target,
|
|
1112
|
+
readOnly: value.readOnly,
|
|
1113
|
+
});
|
|
1114
|
+
requireNetwork(shape.type === 'tmpfs' ? source.length === 0 : source.length > 0);
|
|
1115
|
+
const identityInput: Omit<IFleetCutoverMountDescriptor, 'mountIdentityDigest'> = {
|
|
1116
|
+
...shape,
|
|
1117
|
+
sourceIdentityDigest: await computeFleetCutoverMountSourceIdentityDigest({
|
|
1118
|
+
type: shape.type,
|
|
1119
|
+
source,
|
|
1120
|
+
}),
|
|
1121
|
+
targetIdentityDigest: await computeFleetCutoverMountTargetIdentityDigest({ target }),
|
|
1122
|
+
readOnly: value.readOnly,
|
|
1123
|
+
};
|
|
1124
|
+
return {
|
|
1125
|
+
...identityInput,
|
|
1126
|
+
mountIdentityDigest: await computeFleetCutoverMountIdentityDigest(identityInput),
|
|
1127
|
+
};
|
|
1128
|
+
} catch {
|
|
1129
|
+
return fail();
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
const dockerServiceMountInput = (valueArg: unknown): IFleetCutoverDockerMountInput => {
|
|
1134
|
+
const value = openDockerRecord(valueArg);
|
|
1135
|
+
const type = exactDockerText(value.Type, false);
|
|
1136
|
+
const source = value.Source === undefined ? '' : exactDockerText(value.Source, true);
|
|
1137
|
+
const target = exactDockerText(value.Target, false);
|
|
1138
|
+
const readOnly = value.ReadOnly === undefined ? false : value.ReadOnly;
|
|
1139
|
+
requireNetwork(typeof readOnly === 'boolean');
|
|
1140
|
+
return { type, source, target, readOnly };
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
interface IFleetCutoverDockerLocalMountInput {
|
|
1144
|
+
descriptorInput: IFleetCutoverDockerMountInput;
|
|
1145
|
+
logicalSource: string;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const dockerLocalMountInput = (valueArg: unknown): IFleetCutoverDockerLocalMountInput => {
|
|
1149
|
+
const value = openDockerRecord(valueArg);
|
|
1150
|
+
const type = exactDockerText(value.Type, false);
|
|
1151
|
+
const source = exactDockerText(value.Source, true);
|
|
1152
|
+
const target = exactDockerText(value.Destination, false);
|
|
1153
|
+
requireNetwork(typeof value.RW === 'boolean');
|
|
1154
|
+
const logicalSource = type === 'volume'
|
|
1155
|
+
? exactDockerText(value.Name, false)
|
|
1156
|
+
: source;
|
|
1157
|
+
return {
|
|
1158
|
+
descriptorInput: { type, source, target, readOnly: !value.RW },
|
|
1159
|
+
logicalSource,
|
|
1160
|
+
};
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
/** Read only consumed Service.Spec Mount fields; future Docker response properties are ignored. */
|
|
1164
|
+
export const createFleetCutoverDockerServiceMountDescriptor = async (
|
|
1165
|
+
valueArg: unknown,
|
|
1166
|
+
): Promise<IFleetCutoverMountDescriptor> => createFleetCutoverMountDescriptor(
|
|
1167
|
+
dockerServiceMountInput(valueArg),
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
/** Read only consumed local MountPoint fields and retain the exact physical Source identity. */
|
|
1171
|
+
export const createFleetCutoverDockerLocalMountDescriptor = async (
|
|
1172
|
+
valueArg: unknown,
|
|
1173
|
+
): Promise<IFleetCutoverMountDescriptor> => createFleetCutoverMountDescriptor(
|
|
1174
|
+
dockerLocalMountInput(valueArg).descriptorInput,
|
|
1175
|
+
);
|
|
1176
|
+
|
|
789
1177
|
export const computeFleetCutoverMountSourceIdentityDigest = async (
|
|
790
1178
|
valueArg: IFleetCutoverMountSourceIdentityInput,
|
|
791
1179
|
): Promise<TSha256Digest> => safe(async () => {
|
|
@@ -821,6 +1209,54 @@ export const computeFleetCutoverMountIdentityDigest = async (
|
|
|
821
1209
|
});
|
|
822
1210
|
});
|
|
823
1211
|
|
|
1212
|
+
export const computeFleetCutoverMountCorrelationIdentityDigest = async (
|
|
1213
|
+
valueArg: Omit<IFleetCutoverPhysicalMountCorrelation, 'correlationIdentityDigest'>,
|
|
1214
|
+
): Promise<TSha256Digest> => safe(async () => {
|
|
1215
|
+
const value = networkRecord(valueArg, [
|
|
1216
|
+
'physicalMountIdentityDigest', 'serviceMountIdentityDigest',
|
|
1217
|
+
]);
|
|
1218
|
+
return computeExactIdentityDigest(fleetCutoverContract.mountCorrelationIdentityDigestDomain, {
|
|
1219
|
+
physicalMountIdentityDigest: networkDigest(value.physicalMountIdentityDigest),
|
|
1220
|
+
serviceMountIdentityDigest: networkDigest(value.serviceMountIdentityDigest),
|
|
1221
|
+
});
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Produce both the exact local physical descriptor and its logical service-mount correlation.
|
|
1226
|
+
* For a volume only MountPoint.Name supplies the logical source. Unknown types and a missing or
|
|
1227
|
+
* empty volume Name are refused because they cannot establish a truthful service join.
|
|
1228
|
+
*/
|
|
1229
|
+
export const createFleetCutoverDockerPhysicalMountEvidence = async (
|
|
1230
|
+
valueArg: unknown,
|
|
1231
|
+
): Promise<IFleetCutoverPhysicalMountEvidence> => {
|
|
1232
|
+
try {
|
|
1233
|
+
const value = dockerLocalMountInput(valueArg);
|
|
1234
|
+
requireNetwork(['volume', 'bind', 'tmpfs'].includes(value.descriptorInput.type));
|
|
1235
|
+
const mount = await createFleetCutoverMountDescriptor(value.descriptorInput);
|
|
1236
|
+
const serviceMount = await createFleetCutoverMountDescriptor({
|
|
1237
|
+
...value.descriptorInput,
|
|
1238
|
+
source: value.logicalSource,
|
|
1239
|
+
});
|
|
1240
|
+
const correlationInput: Omit<
|
|
1241
|
+
IFleetCutoverPhysicalMountCorrelation,
|
|
1242
|
+
'correlationIdentityDigest'
|
|
1243
|
+
> = {
|
|
1244
|
+
physicalMountIdentityDigest: mount.mountIdentityDigest,
|
|
1245
|
+
serviceMountIdentityDigest: serviceMount.mountIdentityDigest,
|
|
1246
|
+
};
|
|
1247
|
+
return {
|
|
1248
|
+
mount,
|
|
1249
|
+
correlation: {
|
|
1250
|
+
...correlationInput,
|
|
1251
|
+
correlationIdentityDigest:
|
|
1252
|
+
await computeFleetCutoverMountCorrelationIdentityDigest(correlationInput),
|
|
1253
|
+
},
|
|
1254
|
+
};
|
|
1255
|
+
} catch {
|
|
1256
|
+
return fail();
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
|
|
824
1260
|
const validateMountDescriptors = async (
|
|
825
1261
|
mountsArg: IFleetCutoverMountDescriptor[],
|
|
826
1262
|
): Promise<void> => {
|
|
@@ -830,6 +1266,28 @@ const validateMountDescriptors = async (
|
|
|
830
1266
|
}
|
|
831
1267
|
};
|
|
832
1268
|
|
|
1269
|
+
const validatePhysicalMountCorrelations = async (
|
|
1270
|
+
containerArg: IFleetCutoverPhysicalContainer,
|
|
1271
|
+
): Promise<void> => {
|
|
1272
|
+
if (containerArg.mountCorrelations === undefined) return;
|
|
1273
|
+
requireNetwork(containerArg.mountCorrelations.length === containerArg.mounts.length);
|
|
1274
|
+
const physicalMountIdentities = new Set(
|
|
1275
|
+
containerArg.mounts.map(itemArg => itemArg.mountIdentityDigest),
|
|
1276
|
+
);
|
|
1277
|
+
const serviceMountIdentities = new Set(
|
|
1278
|
+
containerArg.mountCorrelations.map(itemArg => itemArg.serviceMountIdentityDigest),
|
|
1279
|
+
);
|
|
1280
|
+
requireNetwork(physicalMountIdentities.size === containerArg.mounts.length
|
|
1281
|
+
&& serviceMountIdentities.size === containerArg.mountCorrelations.length
|
|
1282
|
+
&& containerArg.mountCorrelations.every(itemArg =>
|
|
1283
|
+
physicalMountIdentities.has(itemArg.physicalMountIdentityDigest)));
|
|
1284
|
+
for (const correlation of containerArg.mountCorrelations) {
|
|
1285
|
+
const { correlationIdentityDigest, ...input } = correlation;
|
|
1286
|
+
requireNetwork(correlationIdentityDigest
|
|
1287
|
+
=== await computeFleetCutoverMountCorrelationIdentityDigest(input));
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
|
|
833
1291
|
export const computeFleetCutoverContainerSetIdentityDigest = async (
|
|
834
1292
|
containerIdsArg: string[],
|
|
835
1293
|
): Promise<TSha256Digest> => safe(async () => {
|
|
@@ -894,7 +1352,10 @@ export const validateFleetCutoverLocalNodeSnapshot = async (
|
|
|
894
1352
|
);
|
|
895
1353
|
requireNetwork(value.before.containerIdsDigest === containerIdsDigest
|
|
896
1354
|
&& value.after.containerIdsDigest === containerIdsDigest);
|
|
897
|
-
for (const container of value.containers)
|
|
1355
|
+
for (const container of value.containers) {
|
|
1356
|
+
await validateMountDescriptors(container.mounts);
|
|
1357
|
+
await validatePhysicalMountCorrelations(container);
|
|
1358
|
+
}
|
|
898
1359
|
return value.snapshotDigest === await computeFleetCutoverLocalNodeSnapshotDigest(value)
|
|
899
1360
|
? [] : ['fleet cutover local node snapshot digest does not match'];
|
|
900
1361
|
} catch {
|
|
@@ -1053,6 +1514,13 @@ export const fleetCutoverEvidenceReferenceOf = (
|
|
|
1053
1514
|
digest: valueArg.digest,
|
|
1054
1515
|
});
|
|
1055
1516
|
|
|
1517
|
+
export const fleetCutoverPreparedVolumeHandoffPageReferenceOf = (
|
|
1518
|
+
valueArg: { id: string; generation: number; digest: TSha256Digest; nodeId: string },
|
|
1519
|
+
): IFleetCutoverPreparedVolumeHandoffPageReference => ({
|
|
1520
|
+
...fleetCutoverEvidenceReferenceOf(valueArg),
|
|
1521
|
+
nodeId: networkNodeId(valueArg.nodeId),
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1056
1524
|
export const snapshotFleetCutoverAcceptedEvidenceReference = (
|
|
1057
1525
|
valueArg: unknown,
|
|
1058
1526
|
): IFleetCutoverAcceptedEvidenceReference => safe(() => reference(captureNetwork(valueArg)));
|
|
@@ -1209,6 +1677,383 @@ export const computeFleetCutoverVolumeHandoffDigest = async (
|
|
|
1209
1677
|
snapshotFleetCutoverVolumeHandoffIntent(valueArg),
|
|
1210
1678
|
);
|
|
1211
1679
|
|
|
1680
|
+
const persistentVolumeMountDescriptor = (
|
|
1681
|
+
valueArg: unknown,
|
|
1682
|
+
): IFleetCutoverPersistentVolumeMountDescriptor => {
|
|
1683
|
+
const value = mountDescriptor(valueArg);
|
|
1684
|
+
requireNetwork(value.type === 'volume'
|
|
1685
|
+
&& value.classification === 'persistent-volume'
|
|
1686
|
+
&& value.readOnly === false);
|
|
1687
|
+
return value as IFleetCutoverPersistentVolumeMountDescriptor;
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
const preparedVolumeHandoff = (
|
|
1691
|
+
valueArg: unknown,
|
|
1692
|
+
): IFleetCutoverPreparedVolumeHandoffIntent => {
|
|
1693
|
+
const value = networkRecord(valueArg, [
|
|
1694
|
+
'id', 'generation', 'clusterId', 'nodeId', 'serviceId', 'volumeId', 'observedMount',
|
|
1695
|
+
'claimIntent', 'runtime', 'scope', 'targetSet', 'legacyStorageEvidence',
|
|
1696
|
+
'sourceNodeObservation', 'acceptedAt', 'digest',
|
|
1697
|
+
]);
|
|
1698
|
+
const clusterId = networkId(value.clusterId);
|
|
1699
|
+
const nodeId = networkNodeId(value.nodeId);
|
|
1700
|
+
const serviceId = networkId(value.serviceId);
|
|
1701
|
+
const observedMount = persistentVolumeMountDescriptor(value.observedMount);
|
|
1702
|
+
const claimIntent = snapshotRuntimeStorageClaimIntent(value.claimIntent);
|
|
1703
|
+
const runtime = runtimeReference(value.runtime);
|
|
1704
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
1705
|
+
requireNetwork(runtime.id === clusterId && runtime.phase === 'draining'
|
|
1706
|
+
&& targetSet.cloudlyClusterId === clusterId
|
|
1707
|
+
&& claimIntent.clusterId === clusterId
|
|
1708
|
+
&& claimIntent.nodeId === nodeId
|
|
1709
|
+
&& claimIntent.serviceId === serviceId);
|
|
1710
|
+
return {
|
|
1711
|
+
id: networkId(value.id),
|
|
1712
|
+
generation: networkCounter(value.generation),
|
|
1713
|
+
clusterId,
|
|
1714
|
+
nodeId,
|
|
1715
|
+
serviceId,
|
|
1716
|
+
volumeId: networkId(value.volumeId),
|
|
1717
|
+
observedMount,
|
|
1718
|
+
claimIntent,
|
|
1719
|
+
runtime: runtime as IFleetCutoverPreparedVolumeHandoffIntent['runtime'],
|
|
1720
|
+
scope: reference(value.scope),
|
|
1721
|
+
targetSet,
|
|
1722
|
+
legacyStorageEvidence: reference(value.legacyStorageEvidence),
|
|
1723
|
+
sourceNodeObservation: reference(value.sourceNodeObservation),
|
|
1724
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
1725
|
+
digest: networkDigest(value.digest),
|
|
1726
|
+
};
|
|
1727
|
+
};
|
|
1728
|
+
|
|
1729
|
+
export const snapshotFleetCutoverPreparedVolumeHandoffIntent = (
|
|
1730
|
+
valueArg: unknown,
|
|
1731
|
+
): IFleetCutoverPreparedVolumeHandoffIntent => safe(
|
|
1732
|
+
() => preparedVolumeHandoff(captureNetwork(valueArg)),
|
|
1733
|
+
);
|
|
1734
|
+
|
|
1735
|
+
export const computeFleetCutoverPreparedVolumeHandoffDigest = async (
|
|
1736
|
+
valueArg: unknown,
|
|
1737
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1738
|
+
fleetCutoverContract.preparedVolumeHandoffDigestDomain,
|
|
1739
|
+
snapshotFleetCutoverPreparedVolumeHandoffIntent(valueArg),
|
|
1740
|
+
);
|
|
1741
|
+
|
|
1742
|
+
export const validateFleetCutoverPreparedVolumeHandoffIntent = async (
|
|
1743
|
+
valueArg: unknown,
|
|
1744
|
+
): Promise<string[]> => {
|
|
1745
|
+
try {
|
|
1746
|
+
const value = snapshotFleetCutoverPreparedVolumeHandoffIntent(valueArg);
|
|
1747
|
+
if ((await validateRuntimeStorageClaimIntent(value.claimIntent)).length) {
|
|
1748
|
+
return ['fleet cutover prepared volume handoff claim intent is invalid'];
|
|
1749
|
+
}
|
|
1750
|
+
return value.digest === await computeFleetCutoverPreparedVolumeHandoffDigest(value)
|
|
1751
|
+
? [] : ['fleet cutover prepared volume handoff digest does not match'];
|
|
1752
|
+
} catch {
|
|
1753
|
+
return ['fleet cutover prepared volume handoff must use its exact canonical schema'];
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
export const snapshotFleetCutoverPreparedVolumeHandoffMutation = (
|
|
1758
|
+
valueArg: unknown,
|
|
1759
|
+
): IFleetCutoverPreparedVolumeHandoffMutation => safe(() => {
|
|
1760
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
1761
|
+
'mutationId', 'clusterId', 'currentRuntime', 'scope', 'targetSet', 'expectedAggregate',
|
|
1762
|
+
'upserts', 'removals',
|
|
1763
|
+
]);
|
|
1764
|
+
const clusterId = networkId(value.clusterId);
|
|
1765
|
+
const currentRuntime = runtimeReference(value.currentRuntime);
|
|
1766
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
1767
|
+
requireNetwork(currentRuntime.id === clusterId && currentRuntime.phase === 'draining'
|
|
1768
|
+
&& targetSet.cloudlyClusterId === clusterId);
|
|
1769
|
+
const requestKeys = new Set<string>();
|
|
1770
|
+
const placementKeys = new Set<string>();
|
|
1771
|
+
const upserts = networkSorted(networkList(
|
|
1772
|
+
value.upserts,
|
|
1773
|
+
fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
|
|
1774
|
+
itemArg => {
|
|
1775
|
+
const item = networkRecord(itemArg, [
|
|
1776
|
+
'nodeId', 'serviceId', 'mountIdentityDigest', 'request',
|
|
1777
|
+
]);
|
|
1778
|
+
const nodeId = networkNodeId(item.nodeId);
|
|
1779
|
+
const serviceId = networkId(item.serviceId);
|
|
1780
|
+
const mountIdentityDigest = networkDigest(item.mountIdentityDigest);
|
|
1781
|
+
const request = normalizeStorageRequest(item.request);
|
|
1782
|
+
requireNetwork(request.kind === 'filesystem'
|
|
1783
|
+
&& request.accessMode === 'ReadWriteOnce'
|
|
1784
|
+
&& request.reclaimPolicy === 'retain');
|
|
1785
|
+
const requestKey = `${nodeId}\u0000${serviceId}\u0000${request.id}`;
|
|
1786
|
+
const placementKey = `${nodeId}\u0000${serviceId}\u0000${mountIdentityDigest}`;
|
|
1787
|
+
requireNetwork(!requestKeys.has(requestKey) && !placementKeys.has(placementKey));
|
|
1788
|
+
requestKeys.add(requestKey);
|
|
1789
|
+
placementKeys.add(placementKey);
|
|
1790
|
+
return {
|
|
1791
|
+
nodeId,
|
|
1792
|
+
serviceId,
|
|
1793
|
+
mountIdentityDigest,
|
|
1794
|
+
request: request as IFleetCutoverPreparedVolumeHandoffUpsert['request'],
|
|
1795
|
+
};
|
|
1796
|
+
},
|
|
1797
|
+
), itemArg => `${itemArg.nodeId}\u0000${itemArg.serviceId}\u0000${itemArg.mountIdentityDigest}`);
|
|
1798
|
+
const removals = networkSorted(networkList(
|
|
1799
|
+
value.removals,
|
|
1800
|
+
fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
|
|
1801
|
+
reference,
|
|
1802
|
+
), itemArg => `${itemArg.id}\u0000${String(itemArg.generation).padStart(16, '0')}\u0000${itemArg.digest}`);
|
|
1803
|
+
requireNetwork(upserts.length + removals.length > 0
|
|
1804
|
+
&& upserts.length + removals.length
|
|
1805
|
+
<= fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries);
|
|
1806
|
+
return {
|
|
1807
|
+
mutationId: networkId(value.mutationId),
|
|
1808
|
+
clusterId,
|
|
1809
|
+
currentRuntime: currentRuntime as IFleetCutoverPreparedVolumeHandoffMutation['currentRuntime'],
|
|
1810
|
+
scope: reference(value.scope),
|
|
1811
|
+
targetSet,
|
|
1812
|
+
expectedAggregate: value.expectedAggregate === null ? null : reference(value.expectedAggregate),
|
|
1813
|
+
upserts,
|
|
1814
|
+
removals,
|
|
1815
|
+
};
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
const preparedVolumeHandoffAggregate = (
|
|
1819
|
+
valueArg: unknown,
|
|
1820
|
+
): IFleetCutoverPreparedVolumeHandoffAggregate => {
|
|
1821
|
+
const value = networkRecord(valueArg, [
|
|
1822
|
+
'id', 'generation', 'previous', 'clusterId', 'runtime', 'scope', 'targetSet', 'state',
|
|
1823
|
+
'requiredHandoffCount', 'handoffCount', 'pageCount', 'handoffSetDigest', 'acceptedAt',
|
|
1824
|
+
'digest',
|
|
1825
|
+
]);
|
|
1826
|
+
const id = networkId(value.id);
|
|
1827
|
+
const generation = networkCounter(value.generation);
|
|
1828
|
+
const previous = value.previous === null ? null : reference(value.previous);
|
|
1829
|
+
requireNetwork(generation === 1
|
|
1830
|
+
? previous === null
|
|
1831
|
+
: previous?.id === id && previous.generation === generation - 1);
|
|
1832
|
+
const clusterId = networkId(value.clusterId);
|
|
1833
|
+
const runtime = runtimeReference(value.runtime);
|
|
1834
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
1835
|
+
const state = literal(['incomplete', 'complete'] as const, value.state);
|
|
1836
|
+
const requiredHandoffCount = nonNegative(value.requiredHandoffCount);
|
|
1837
|
+
const handoffCount = nonNegative(value.handoffCount);
|
|
1838
|
+
const pageCount = nonNegative(value.pageCount);
|
|
1839
|
+
requireNetwork(runtime.id === clusterId && runtime.phase === 'draining'
|
|
1840
|
+
&& targetSet.cloudlyClusterId === clusterId
|
|
1841
|
+
&& requiredHandoffCount <= fleetCutoverContract.maximumPreparedVolumeHandoffs
|
|
1842
|
+
&& handoffCount <= fleetCutoverContract.maximumPreparedVolumeHandoffs
|
|
1843
|
+
&& pageCount <= fleetCutoverContract.maximumPreparedVolumeHandoffPages
|
|
1844
|
+
&& pageCount === Math.ceil(handoffCount
|
|
1845
|
+
/ fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries)
|
|
1846
|
+
&& (state === 'complete') === (requiredHandoffCount === handoffCount));
|
|
1847
|
+
return {
|
|
1848
|
+
id,
|
|
1849
|
+
generation,
|
|
1850
|
+
previous,
|
|
1851
|
+
clusterId,
|
|
1852
|
+
runtime: runtime as IFleetCutoverPreparedVolumeHandoffAggregate['runtime'],
|
|
1853
|
+
scope: reference(value.scope),
|
|
1854
|
+
targetSet,
|
|
1855
|
+
state,
|
|
1856
|
+
requiredHandoffCount,
|
|
1857
|
+
handoffCount,
|
|
1858
|
+
pageCount,
|
|
1859
|
+
handoffSetDigest: networkDigest(value.handoffSetDigest),
|
|
1860
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
1861
|
+
digest: networkDigest(value.digest),
|
|
1862
|
+
};
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
export const snapshotFleetCutoverPreparedVolumeHandoffAggregate = (
|
|
1866
|
+
valueArg: unknown,
|
|
1867
|
+
): IFleetCutoverPreparedVolumeHandoffAggregate => safe(
|
|
1868
|
+
() => preparedVolumeHandoffAggregate(captureNetwork(valueArg)),
|
|
1869
|
+
);
|
|
1870
|
+
|
|
1871
|
+
export const computeFleetCutoverPreparedVolumeHandoffAggregateDigest = async (
|
|
1872
|
+
valueArg: unknown,
|
|
1873
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1874
|
+
fleetCutoverContract.preparedVolumeHandoffAggregateDigestDomain,
|
|
1875
|
+
snapshotFleetCutoverPreparedVolumeHandoffAggregate(valueArg),
|
|
1876
|
+
);
|
|
1877
|
+
|
|
1878
|
+
export const validateFleetCutoverPreparedVolumeHandoffAggregate = async (
|
|
1879
|
+
valueArg: unknown,
|
|
1880
|
+
): Promise<string[]> => {
|
|
1881
|
+
try {
|
|
1882
|
+
const value = snapshotFleetCutoverPreparedVolumeHandoffAggregate(valueArg);
|
|
1883
|
+
return value.digest === await computeFleetCutoverPreparedVolumeHandoffAggregateDigest(value)
|
|
1884
|
+
? [] : ['fleet cutover prepared volume handoff aggregate digest does not match'];
|
|
1885
|
+
} catch {
|
|
1886
|
+
return ['fleet cutover prepared volume handoff aggregate must use its exact canonical schema'];
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
|
|
1890
|
+
const preparedVolumeHandoffPage = (
|
|
1891
|
+
valueArg: unknown,
|
|
1892
|
+
): IFleetCutoverPreparedVolumeHandoffPage => {
|
|
1893
|
+
const value = networkRecord(valueArg, [
|
|
1894
|
+
'id', 'generation', 'aggregate', 'pageIndex', 'handoffs', 'handoffSetPageDigest', 'digest',
|
|
1895
|
+
]);
|
|
1896
|
+
requireNetwork(value.generation === 1);
|
|
1897
|
+
const handoffs = networkSorted(networkList(
|
|
1898
|
+
value.handoffs,
|
|
1899
|
+
fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
|
|
1900
|
+
itemArg => {
|
|
1901
|
+
const item = networkRecord(itemArg, ['id', 'generation', 'digest', 'nodeId']);
|
|
1902
|
+
return fleetCutoverPreparedVolumeHandoffPageReferenceOf({
|
|
1903
|
+
...reference({ id: item.id, generation: item.generation, digest: item.digest }),
|
|
1904
|
+
nodeId: networkNodeId(item.nodeId),
|
|
1905
|
+
});
|
|
1906
|
+
},
|
|
1907
|
+
), itemArg => `${itemArg.id}\u0000${String(itemArg.generation).padStart(16, '0')}\u0000${itemArg.digest}`);
|
|
1908
|
+
const pageIndex = nonNegative(value.pageIndex);
|
|
1909
|
+
requireNetwork(handoffs.length > 0
|
|
1910
|
+
&& pageIndex < fleetCutoverContract.maximumPreparedVolumeHandoffPages);
|
|
1911
|
+
return {
|
|
1912
|
+
id: networkId(value.id),
|
|
1913
|
+
generation: 1,
|
|
1914
|
+
aggregate: reference(value.aggregate),
|
|
1915
|
+
pageIndex,
|
|
1916
|
+
handoffs,
|
|
1917
|
+
handoffSetPageDigest: networkDigest(value.handoffSetPageDigest),
|
|
1918
|
+
digest: networkDigest(value.digest),
|
|
1919
|
+
};
|
|
1920
|
+
};
|
|
1921
|
+
|
|
1922
|
+
export const snapshotFleetCutoverPreparedVolumeHandoffPage = (
|
|
1923
|
+
valueArg: unknown,
|
|
1924
|
+
): IFleetCutoverPreparedVolumeHandoffPage => safe(
|
|
1925
|
+
() => preparedVolumeHandoffPage(captureNetwork(valueArg)),
|
|
1926
|
+
);
|
|
1927
|
+
|
|
1928
|
+
export const computeFleetCutoverPreparedVolumeHandoffPageContentDigest = async (
|
|
1929
|
+
pageIndexArg: unknown,
|
|
1930
|
+
handoffsArg: unknown,
|
|
1931
|
+
): Promise<TSha256Digest> => {
|
|
1932
|
+
const page = preparedVolumeHandoffPage({
|
|
1933
|
+
id: 'FleetCutoverPreparedVolumeHandoffPage:content',
|
|
1934
|
+
generation: 1,
|
|
1935
|
+
aggregate: { id: 'FleetCutoverPreparedVolumeHandoffAggregate:content', generation: 1,
|
|
1936
|
+
digest: `sha256:${'0'.repeat(64)}` },
|
|
1937
|
+
pageIndex: pageIndexArg,
|
|
1938
|
+
handoffs: handoffsArg,
|
|
1939
|
+
handoffSetPageDigest: `sha256:${'0'.repeat(64)}`,
|
|
1940
|
+
digest: `sha256:${'0'.repeat(64)}`,
|
|
1941
|
+
});
|
|
1942
|
+
return canonicalDigest(fleetCutoverContract.preparedVolumeHandoffPageContentDigestDomain, {
|
|
1943
|
+
pageIndex: page.pageIndex,
|
|
1944
|
+
handoffs: page.handoffs,
|
|
1945
|
+
digest: `sha256:${'0'.repeat(64)}`,
|
|
1946
|
+
});
|
|
1947
|
+
};
|
|
1948
|
+
|
|
1949
|
+
export const computeFleetCutoverPreparedVolumeHandoffPageDigest = async (
|
|
1950
|
+
valueArg: unknown,
|
|
1951
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1952
|
+
fleetCutoverContract.preparedVolumeHandoffPageDigestDomain,
|
|
1953
|
+
snapshotFleetCutoverPreparedVolumeHandoffPage(valueArg),
|
|
1954
|
+
);
|
|
1955
|
+
|
|
1956
|
+
export const validateFleetCutoverPreparedVolumeHandoffPage = async (
|
|
1957
|
+
valueArg: unknown,
|
|
1958
|
+
): Promise<string[]> => {
|
|
1959
|
+
try {
|
|
1960
|
+
const value = snapshotFleetCutoverPreparedVolumeHandoffPage(valueArg);
|
|
1961
|
+
if (value.handoffSetPageDigest
|
|
1962
|
+
!== await computeFleetCutoverPreparedVolumeHandoffPageContentDigest(
|
|
1963
|
+
value.pageIndex,
|
|
1964
|
+
value.handoffs,
|
|
1965
|
+
)) {
|
|
1966
|
+
return ['fleet cutover prepared volume handoff page content digest does not match'];
|
|
1967
|
+
}
|
|
1968
|
+
return value.digest === await computeFleetCutoverPreparedVolumeHandoffPageDigest(value)
|
|
1969
|
+
? [] : ['fleet cutover prepared volume handoff page digest does not match'];
|
|
1970
|
+
} catch {
|
|
1971
|
+
return ['fleet cutover prepared volume handoff page must use its exact canonical schema'];
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
|
|
1975
|
+
export const computeFleetCutoverPreparedVolumeHandoffSetDigest = async (
|
|
1976
|
+
entriesArg: unknown,
|
|
1977
|
+
): Promise<TSha256Digest> => safe(async () => {
|
|
1978
|
+
const entries = networkSorted(networkList(
|
|
1979
|
+
captureNetwork(entriesArg),
|
|
1980
|
+
fleetCutoverContract.maximumPreparedVolumeHandoffPages,
|
|
1981
|
+
itemArg => {
|
|
1982
|
+
const item = networkRecord(itemArg, ['pageIndex', 'handoffSetPageDigest']);
|
|
1983
|
+
return {
|
|
1984
|
+
pageIndex: nonNegative(item.pageIndex),
|
|
1985
|
+
handoffSetPageDigest: networkDigest(item.handoffSetPageDigest),
|
|
1986
|
+
};
|
|
1987
|
+
},
|
|
1988
|
+
), itemArg => String(itemArg.pageIndex).padStart(8, '0'));
|
|
1989
|
+
requireNetwork(entries.every((entryArg, indexArg) => entryArg.pageIndex === indexArg));
|
|
1990
|
+
return canonicalDigest(fleetCutoverContract.preparedVolumeHandoffSetDigestDomain, {
|
|
1991
|
+
entries,
|
|
1992
|
+
digest: `sha256:${'0'.repeat(64)}`,
|
|
1993
|
+
});
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
/**
|
|
1997
|
+
* Full pinned-set proof. The caller must supply every page for the exact aggregate revision; a
|
|
1998
|
+
* node-local subset or a mixed revision can never satisfy this binder.
|
|
1999
|
+
*/
|
|
2000
|
+
export const bindFleetCutoverPreparedVolumeHandoffPages = async (
|
|
2001
|
+
aggregateArg: unknown,
|
|
2002
|
+
pagesArg: unknown,
|
|
2003
|
+
): Promise<{
|
|
2004
|
+
aggregate: IFleetCutoverPreparedVolumeHandoffAggregate;
|
|
2005
|
+
pages: IFleetCutoverPreparedVolumeHandoffPage[];
|
|
2006
|
+
}> => {
|
|
2007
|
+
const aggregate = snapshotFleetCutoverPreparedVolumeHandoffAggregate(aggregateArg);
|
|
2008
|
+
if ((await validateFleetCutoverPreparedVolumeHandoffAggregate(aggregate)).length) fail();
|
|
2009
|
+
const pages = safe(() => {
|
|
2010
|
+
requireNetwork(Array.isArray(pagesArg)
|
|
2011
|
+
&& Object.getPrototypeOf(pagesArg) === Array.prototype
|
|
2012
|
+
&& pagesArg.length <= fleetCutoverContract.maximumPreparedVolumeHandoffPages);
|
|
2013
|
+
const keys = Reflect.ownKeys(pagesArg);
|
|
2014
|
+
requireNetwork(keys.length === pagesArg.length + 1);
|
|
2015
|
+
const result: IFleetCutoverPreparedVolumeHandoffPage[] = [];
|
|
2016
|
+
for (let index = 0; index < pagesArg.length; index++) {
|
|
2017
|
+
const descriptor = Object.getOwnPropertyDescriptor(pagesArg, String(index));
|
|
2018
|
+
requireNetwork(descriptor?.enumerable && Object.hasOwn(descriptor, 'value'));
|
|
2019
|
+
result.push(snapshotFleetCutoverPreparedVolumeHandoffPage(descriptor.value));
|
|
2020
|
+
}
|
|
2021
|
+
return networkSorted(result, itemArg => String(itemArg.pageIndex).padStart(8, '0'));
|
|
2022
|
+
});
|
|
2023
|
+
requireNetwork(pages.length === aggregate.pageCount);
|
|
2024
|
+
let handoffCount = 0;
|
|
2025
|
+
const seen = new Set<string>();
|
|
2026
|
+
let previousHandoffKey: string | null = null;
|
|
2027
|
+
for (const [index, page] of pages.entries()) {
|
|
2028
|
+
requireNetwork(page.pageIndex === index
|
|
2029
|
+
&& page.aggregate.id === aggregate.id
|
|
2030
|
+
&& page.aggregate.generation === aggregate.generation
|
|
2031
|
+
&& page.aggregate.digest === aggregate.digest
|
|
2032
|
+
&& !(await validateFleetCutoverPreparedVolumeHandoffPage(page)).length);
|
|
2033
|
+
const expectedLength = index < pages.length - 1
|
|
2034
|
+
? fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries
|
|
2035
|
+
: aggregate.handoffCount
|
|
2036
|
+
% fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries
|
|
2037
|
+
|| fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries;
|
|
2038
|
+
requireNetwork(page.handoffs.length === expectedLength);
|
|
2039
|
+
handoffCount += page.handoffs.length;
|
|
2040
|
+
for (const handoff of page.handoffs) {
|
|
2041
|
+
const key = `${handoff.id}\u0000${handoff.generation}\u0000${handoff.digest}`;
|
|
2042
|
+
requireNetwork(!seen.has(key) && (previousHandoffKey === null || previousHandoffKey < key));
|
|
2043
|
+
seen.add(key);
|
|
2044
|
+
previousHandoffKey = key;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
const entries = pages.map(page => ({
|
|
2048
|
+
pageIndex: page.pageIndex,
|
|
2049
|
+
handoffSetPageDigest: page.handoffSetPageDigest,
|
|
2050
|
+
}));
|
|
2051
|
+
requireNetwork(handoffCount === aggregate.handoffCount
|
|
2052
|
+
&& aggregate.handoffSetDigest
|
|
2053
|
+
=== await computeFleetCutoverPreparedVolumeHandoffSetDigest(entries));
|
|
2054
|
+
return { aggregate, pages };
|
|
2055
|
+
};
|
|
2056
|
+
|
|
1212
2057
|
const volumeFence = (valueArg: unknown): TFleetCutoverVolumeFence => {
|
|
1213
2058
|
requireNetwork(valueArg !== null && typeof valueArg === 'object');
|
|
1214
2059
|
const state = (valueArg as Record<string, unknown>).state;
|
|
@@ -1324,6 +2169,45 @@ export const bindFleetCutoverAdoptionToVolumeFence = (
|
|
|
1324
2169
|
return { adoption, fence };
|
|
1325
2170
|
});
|
|
1326
2171
|
|
|
2172
|
+
/**
|
|
2173
|
+
* Joins the accepted adoption result to the full handoff and provider-ready claim content.
|
|
2174
|
+
* Native attachment fencing and accepted-record dereferencing remain owner checks.
|
|
2175
|
+
*/
|
|
2176
|
+
export const bindFleetCutoverAdoptionToStorageClaim = async (
|
|
2177
|
+
adoptionArg: unknown,
|
|
2178
|
+
handoffArg: unknown,
|
|
2179
|
+
claimArg: unknown,
|
|
2180
|
+
): Promise<{
|
|
2181
|
+
adoption: IFleetCutoverCorestoreAdoptionReceipt;
|
|
2182
|
+
handoff: IFleetCutoverPreparedVolumeHandoffIntent;
|
|
2183
|
+
claim: IRuntimeStorageClaim;
|
|
2184
|
+
}> => {
|
|
2185
|
+
try {
|
|
2186
|
+
const adoption = snapshotFleetCutoverCorestoreAdoptionReceipt(adoptionArg);
|
|
2187
|
+
const handoff = snapshotFleetCutoverPreparedVolumeHandoffIntent(handoffArg);
|
|
2188
|
+
const claim = snapshotRuntimeStorageClaim(claimArg);
|
|
2189
|
+
if ((await validateFleetCutoverCorestoreAdoptionReceipt(adoption)).length
|
|
2190
|
+
|| (await validateFleetCutoverPreparedVolumeHandoffIntent(handoff)).length
|
|
2191
|
+
|| (await validateRuntimeStorageClaim(claim)).length) fail();
|
|
2192
|
+
await bindRuntimeStorageClaimToIntent(claim, handoff.claimIntent);
|
|
2193
|
+
requireNetwork(adoption.clusterId === handoff.clusterId
|
|
2194
|
+
&& adoption.nodeId === handoff.nodeId
|
|
2195
|
+
&& adoption.serviceId === handoff.serviceId
|
|
2196
|
+
&& adoption.volumeId === handoff.volumeId
|
|
2197
|
+
&& adoption.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
|
|
2198
|
+
&& adoption.handoffIntent.id === handoff.id
|
|
2199
|
+
&& adoption.handoffIntent.generation === handoff.generation
|
|
2200
|
+
&& adoption.handoffIntent.digest === handoff.digest
|
|
2201
|
+
&& adoption.claim.id === claim.id
|
|
2202
|
+
&& adoption.claim.generation === claim.generation
|
|
2203
|
+
&& adoption.claim.digest === claim.digest
|
|
2204
|
+
&& adoption.acceptedAt >= handoff.acceptedAt);
|
|
2205
|
+
return { adoption, handoff, claim };
|
|
2206
|
+
} catch {
|
|
2207
|
+
return fail();
|
|
2208
|
+
}
|
|
2209
|
+
};
|
|
2210
|
+
|
|
1327
2211
|
export const validateFleetCutoverVolumeHandoffIntent = async (valueArg: unknown): Promise<string[]> => {
|
|
1328
2212
|
try {
|
|
1329
2213
|
const value = snapshotFleetCutoverVolumeHandoffIntent(valueArg);
|
|
@@ -1373,6 +2257,273 @@ const runtimeSession = (valueArg: unknown): IRuntimeSessionBinding => {
|
|
|
1373
2257
|
};
|
|
1374
2258
|
};
|
|
1375
2259
|
|
|
2260
|
+
const storageAdoptionGrant = (valueArg: unknown): IFleetCutoverStorageAdoptionGrant => {
|
|
2261
|
+
const value = networkRecord(valueArg, [
|
|
2262
|
+
'id', 'generation', 'previous', 'clusterId', 'nodeId', 'serviceId', 'volumeId',
|
|
2263
|
+
'observedMount', 'runtime', 'session', 'handoffIntent', 'palletFence', 'acceptedAt',
|
|
2264
|
+
'digest',
|
|
2265
|
+
]);
|
|
2266
|
+
const id = networkId(value.id);
|
|
2267
|
+
const generation = networkCounter(value.generation);
|
|
2268
|
+
const previous = value.previous === null ? null : reference(value.previous);
|
|
2269
|
+
requireNetwork(generation === 1
|
|
2270
|
+
? previous === null
|
|
2271
|
+
: previous?.id === id && previous.generation === generation - 1);
|
|
2272
|
+
const clusterId = networkId(value.clusterId);
|
|
2273
|
+
const nodeId = networkNodeId(value.nodeId);
|
|
2274
|
+
const runtime = runtimeReference(value.runtime);
|
|
2275
|
+
const session = runtimeSession(value.session);
|
|
2276
|
+
requireNetwork(runtime.id === clusterId && runtime.phase === 'pallet'
|
|
2277
|
+
&& session.clusterId === clusterId
|
|
2278
|
+
&& session.nodeId === nodeId
|
|
2279
|
+
&& session.controller.kind === 'cloudly');
|
|
2280
|
+
return {
|
|
2281
|
+
id,
|
|
2282
|
+
generation,
|
|
2283
|
+
previous,
|
|
2284
|
+
clusterId,
|
|
2285
|
+
nodeId,
|
|
2286
|
+
serviceId: networkId(value.serviceId),
|
|
2287
|
+
volumeId: networkId(value.volumeId),
|
|
2288
|
+
observedMount: persistentVolumeMountDescriptor(value.observedMount),
|
|
2289
|
+
runtime: runtime as IFleetCutoverStorageAdoptionGrant['runtime'],
|
|
2290
|
+
session,
|
|
2291
|
+
handoffIntent: reference(value.handoffIntent),
|
|
2292
|
+
palletFence: reference(value.palletFence),
|
|
2293
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
2294
|
+
digest: networkDigest(value.digest),
|
|
2295
|
+
};
|
|
2296
|
+
};
|
|
2297
|
+
|
|
2298
|
+
export const snapshotFleetCutoverStorageAdoptionGrant = (
|
|
2299
|
+
valueArg: unknown,
|
|
2300
|
+
): IFleetCutoverStorageAdoptionGrant => safe(
|
|
2301
|
+
() => storageAdoptionGrant(captureNetwork(valueArg)),
|
|
2302
|
+
);
|
|
2303
|
+
|
|
2304
|
+
export const computeFleetCutoverStorageAdoptionGrantDigest = async (
|
|
2305
|
+
valueArg: unknown,
|
|
2306
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
2307
|
+
fleetCutoverContract.storageAdoptionGrantDigestDomain,
|
|
2308
|
+
snapshotFleetCutoverStorageAdoptionGrant(valueArg),
|
|
2309
|
+
);
|
|
2310
|
+
|
|
2311
|
+
export const validateFleetCutoverStorageAdoptionGrant = async (
|
|
2312
|
+
valueArg: unknown,
|
|
2313
|
+
): Promise<string[]> => {
|
|
2314
|
+
try {
|
|
2315
|
+
const value = snapshotFleetCutoverStorageAdoptionGrant(valueArg);
|
|
2316
|
+
return value.digest === await computeFleetCutoverStorageAdoptionGrantDigest(value)
|
|
2317
|
+
? [] : ['fleet cutover storage adoption grant digest does not match'];
|
|
2318
|
+
} catch {
|
|
2319
|
+
return ['fleet cutover storage adoption grant must use its exact canonical schema'];
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
|
|
2323
|
+
/** Content join only; the controller must still prove this is the live current session and phase. */
|
|
2324
|
+
export const bindFleetCutoverStorageAdoptionGrant = async (
|
|
2325
|
+
grantArg: unknown,
|
|
2326
|
+
handoffArg: unknown,
|
|
2327
|
+
heldFenceArg: unknown,
|
|
2328
|
+
): Promise<{
|
|
2329
|
+
grant: IFleetCutoverStorageAdoptionGrant;
|
|
2330
|
+
handoff: IFleetCutoverPreparedVolumeHandoffIntent;
|
|
2331
|
+
fence: Extract<TFleetCutoverVolumeFence, { state: 'held' }>;
|
|
2332
|
+
}> => {
|
|
2333
|
+
try {
|
|
2334
|
+
const grant = snapshotFleetCutoverStorageAdoptionGrant(grantArg);
|
|
2335
|
+
const handoff = snapshotFleetCutoverPreparedVolumeHandoffIntent(handoffArg);
|
|
2336
|
+
const fence = snapshotFleetCutoverVolumeFence(heldFenceArg);
|
|
2337
|
+
if ((await validateFleetCutoverStorageAdoptionGrant(grant)).length
|
|
2338
|
+
|| (await validateFleetCutoverPreparedVolumeHandoffIntent(handoff)).length
|
|
2339
|
+
|| (await validateFleetCutoverVolumeFence(fence)).length) fail();
|
|
2340
|
+
requireNetwork(fence.state === 'held'
|
|
2341
|
+
&& grant.clusterId === handoff.clusterId
|
|
2342
|
+
&& grant.nodeId === handoff.nodeId
|
|
2343
|
+
&& grant.serviceId === handoff.serviceId
|
|
2344
|
+
&& grant.volumeId === handoff.volumeId
|
|
2345
|
+
&& grant.observedMount.type === handoff.observedMount.type
|
|
2346
|
+
&& grant.observedMount.classification === handoff.observedMount.classification
|
|
2347
|
+
&& grant.observedMount.sourceIdentityDigest === handoff.observedMount.sourceIdentityDigest
|
|
2348
|
+
&& grant.observedMount.targetIdentityDigest === handoff.observedMount.targetIdentityDigest
|
|
2349
|
+
&& grant.observedMount.readOnly === handoff.observedMount.readOnly
|
|
2350
|
+
&& grant.observedMount.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
|
|
2351
|
+
&& grant.handoffIntent.id === handoff.id
|
|
2352
|
+
&& grant.handoffIntent.generation === handoff.generation
|
|
2353
|
+
&& grant.handoffIntent.digest === handoff.digest
|
|
2354
|
+
&& grant.palletFence.id === fence.id
|
|
2355
|
+
&& grant.palletFence.generation === fence.generation
|
|
2356
|
+
&& grant.palletFence.digest === fence.digest
|
|
2357
|
+
&& fence.clusterId === handoff.clusterId
|
|
2358
|
+
&& fence.nodeId === handoff.nodeId
|
|
2359
|
+
&& fence.serviceId === handoff.serviceId
|
|
2360
|
+
&& fence.volumeId === handoff.volumeId
|
|
2361
|
+
&& fence.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
|
|
2362
|
+
&& fence.handoffIntent.id === handoff.id
|
|
2363
|
+
&& fence.handoffIntent.generation === handoff.generation
|
|
2364
|
+
&& fence.handoffIntent.digest === handoff.digest
|
|
2365
|
+
&& fence.heldRuntime.id === handoff.runtime.id
|
|
2366
|
+
&& fence.heldRuntime.phase === handoff.runtime.phase
|
|
2367
|
+
&& fence.heldRuntime.generation === handoff.runtime.generation
|
|
2368
|
+
&& grant.session.controller.id === handoff.claimIntent.controller.id
|
|
2369
|
+
&& grant.session.controller.epoch === handoff.claimIntent.controller.epoch
|
|
2370
|
+
&& grant.session.runtimeNamespace === handoff.claimIntent.runtimeNamespace
|
|
2371
|
+
&& grant.acceptedAt >= fence.heldAt);
|
|
2372
|
+
return { grant, handoff, fence };
|
|
2373
|
+
} catch {
|
|
2374
|
+
return fail();
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
2377
|
+
|
|
2378
|
+
export const snapshotFleetCutoverStorageAdoptionBundle = (
|
|
2379
|
+
valueArg: unknown,
|
|
2380
|
+
): IFleetCutoverStorageAdoptionBundle => safe(() => {
|
|
2381
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
2382
|
+
'grant', 'fence', 'adoption', 'claim',
|
|
2383
|
+
]);
|
|
2384
|
+
const fence = snapshotFleetCutoverVolumeFence(value.fence);
|
|
2385
|
+
requireNetwork(fence.state === 'consumed');
|
|
2386
|
+
return {
|
|
2387
|
+
grant: snapshotFleetCutoverStorageAdoptionGrant(value.grant),
|
|
2388
|
+
fence,
|
|
2389
|
+
adoption: snapshotFleetCutoverCorestoreAdoptionReceipt(value.adoption),
|
|
2390
|
+
claim: snapshotRuntimeStorageClaim(value.claim),
|
|
2391
|
+
};
|
|
2392
|
+
});
|
|
2393
|
+
|
|
2394
|
+
/**
|
|
2395
|
+
* Binds the complete post-adoption content. Physical lineage remains the provider-produced
|
|
2396
|
+
* adoption digest and is deliberately never equated to the observed Docker source identity.
|
|
2397
|
+
*/
|
|
2398
|
+
export const bindFleetCutoverStorageAdoptionBundle = async (
|
|
2399
|
+
valueArg: unknown,
|
|
2400
|
+
handoffArg: unknown,
|
|
2401
|
+
): Promise<{
|
|
2402
|
+
bundle: IFleetCutoverStorageAdoptionBundle;
|
|
2403
|
+
handoff: IFleetCutoverPreparedVolumeHandoffIntent;
|
|
2404
|
+
}> => {
|
|
2405
|
+
try {
|
|
2406
|
+
const bundle = snapshotFleetCutoverStorageAdoptionBundle(valueArg);
|
|
2407
|
+
const handoff = snapshotFleetCutoverPreparedVolumeHandoffIntent(handoffArg);
|
|
2408
|
+
const { grant, fence, adoption, claim } = bundle;
|
|
2409
|
+
if ((await validateFleetCutoverStorageAdoptionGrant(grant)).length
|
|
2410
|
+
|| (await validateFleetCutoverPreparedVolumeHandoffIntent(handoff)).length
|
|
2411
|
+
|| (await validateFleetCutoverVolumeFence(fence)).length
|
|
2412
|
+
|| (await validateFleetCutoverCorestoreAdoptionReceipt(adoption)).length
|
|
2413
|
+
|| (await validateRuntimeStorageClaim(claim)).length) fail();
|
|
2414
|
+
await bindRuntimeStorageClaimToIntent(claim, handoff.claimIntent);
|
|
2415
|
+
requireNetwork(grant.clusterId === handoff.clusterId
|
|
2416
|
+
&& grant.nodeId === handoff.nodeId
|
|
2417
|
+
&& grant.serviceId === handoff.serviceId
|
|
2418
|
+
&& grant.volumeId === handoff.volumeId
|
|
2419
|
+
&& grant.observedMount.type === handoff.observedMount.type
|
|
2420
|
+
&& grant.observedMount.classification === handoff.observedMount.classification
|
|
2421
|
+
&& grant.observedMount.sourceIdentityDigest === handoff.observedMount.sourceIdentityDigest
|
|
2422
|
+
&& grant.observedMount.targetIdentityDigest === handoff.observedMount.targetIdentityDigest
|
|
2423
|
+
&& grant.observedMount.readOnly === handoff.observedMount.readOnly
|
|
2424
|
+
&& grant.observedMount.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
|
|
2425
|
+
&& grant.session.controller.id === handoff.claimIntent.controller.id
|
|
2426
|
+
&& grant.session.controller.epoch === handoff.claimIntent.controller.epoch
|
|
2427
|
+
&& grant.session.runtimeNamespace === handoff.claimIntent.runtimeNamespace
|
|
2428
|
+
&& grant.handoffIntent.id === handoff.id
|
|
2429
|
+
&& grant.handoffIntent.generation === handoff.generation
|
|
2430
|
+
&& grant.handoffIntent.digest === handoff.digest
|
|
2431
|
+
&& fence.clusterId === grant.clusterId
|
|
2432
|
+
&& fence.nodeId === grant.nodeId
|
|
2433
|
+
&& fence.serviceId === grant.serviceId
|
|
2434
|
+
&& fence.volumeId === grant.volumeId
|
|
2435
|
+
&& fence.mountIdentityDigest === grant.observedMount.mountIdentityDigest
|
|
2436
|
+
&& fence.handoffIntent.id === handoff.id
|
|
2437
|
+
&& fence.handoffIntent.generation === handoff.generation
|
|
2438
|
+
&& fence.handoffIntent.digest === handoff.digest
|
|
2439
|
+
&& fence.previous !== null
|
|
2440
|
+
&& fence.previous.id === grant.palletFence.id
|
|
2441
|
+
&& fence.previous.generation === grant.palletFence.generation
|
|
2442
|
+
&& fence.previous.digest === grant.palletFence.digest
|
|
2443
|
+
&& fence.consumedRuntime.id === grant.runtime.id
|
|
2444
|
+
&& fence.consumedRuntime.phase === grant.runtime.phase
|
|
2445
|
+
&& fence.consumedRuntime.generation === grant.runtime.generation
|
|
2446
|
+
&& adoption.clusterId === handoff.clusterId
|
|
2447
|
+
&& adoption.nodeId === handoff.nodeId
|
|
2448
|
+
&& adoption.serviceId === handoff.serviceId
|
|
2449
|
+
&& adoption.volumeId === handoff.volumeId
|
|
2450
|
+
&& adoption.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
|
|
2451
|
+
&& adoption.handoffIntent.id === handoff.id
|
|
2452
|
+
&& adoption.handoffIntent.generation === handoff.generation
|
|
2453
|
+
&& adoption.handoffIntent.digest === handoff.digest
|
|
2454
|
+
&& adoption.palletFence.id === grant.palletFence.id
|
|
2455
|
+
&& adoption.palletFence.generation === grant.palletFence.generation
|
|
2456
|
+
&& adoption.palletFence.digest === grant.palletFence.digest
|
|
2457
|
+
&& adoption.claim.id === claim.id
|
|
2458
|
+
&& adoption.claim.generation === claim.generation
|
|
2459
|
+
&& adoption.claim.digest === claim.digest
|
|
2460
|
+
&& fence.adoptionReceipt.id === adoption.id
|
|
2461
|
+
&& fence.adoptionReceipt.generation === adoption.generation
|
|
2462
|
+
&& fence.adoptionReceipt.digest === adoption.digest
|
|
2463
|
+
&& fence.claim.id === claim.id
|
|
2464
|
+
&& fence.claim.generation === claim.generation
|
|
2465
|
+
&& fence.claim.digest === claim.digest
|
|
2466
|
+
&& adoption.acceptedAt >= grant.acceptedAt
|
|
2467
|
+
&& fence.consumedAt >= adoption.acceptedAt);
|
|
2468
|
+
return { bundle, handoff };
|
|
2469
|
+
} catch {
|
|
2470
|
+
return fail();
|
|
2471
|
+
}
|
|
2472
|
+
};
|
|
2473
|
+
|
|
2474
|
+
export const snapshotFleetCutoverHeldVolumeFenceEvidencePage = (
|
|
2475
|
+
valueArg: unknown,
|
|
2476
|
+
): IFleetCutoverHeldVolumeFenceEvidencePage => safe(() => {
|
|
2477
|
+
const value = networkRecord(captureNetwork(valueArg), ['aggregate', 'page', 'nodeId', 'fences']);
|
|
2478
|
+
const aggregate = reference(value.aggregate);
|
|
2479
|
+
const page = preparedVolumeHandoffPage(value.page);
|
|
2480
|
+
const nodeId = networkNodeId(value.nodeId);
|
|
2481
|
+
requireNetwork(page.aggregate.id === aggregate.id
|
|
2482
|
+
&& page.aggregate.generation === aggregate.generation
|
|
2483
|
+
&& page.aggregate.digest === aggregate.digest);
|
|
2484
|
+
const pageHandoffs = page.handoffs.filter(itemArg => itemArg.nodeId === nodeId);
|
|
2485
|
+
const fences = networkSorted(networkList(
|
|
2486
|
+
value.fences,
|
|
2487
|
+
fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
|
|
2488
|
+
itemArg => {
|
|
2489
|
+
const fence = volumeFence(itemArg);
|
|
2490
|
+
requireNetwork(fence.state === 'held' && fence.nodeId === nodeId);
|
|
2491
|
+
return fence;
|
|
2492
|
+
},
|
|
2493
|
+
), itemArg => `${itemArg.handoffIntent.id}\u0000${itemArg.handoffIntent.generation}\u0000${itemArg.handoffIntent.digest}`);
|
|
2494
|
+
requireNetwork(fences.length === pageHandoffs.length
|
|
2495
|
+
&& fences.every((fenceArg, indexArg) => {
|
|
2496
|
+
const handoff = pageHandoffs[indexArg];
|
|
2497
|
+
return fenceArg.handoffIntent.id === handoff.id
|
|
2498
|
+
&& fenceArg.handoffIntent.generation === handoff.generation
|
|
2499
|
+
&& fenceArg.handoffIntent.digest === handoff.digest;
|
|
2500
|
+
}));
|
|
2501
|
+
return { aggregate, page, nodeId, fences };
|
|
2502
|
+
});
|
|
2503
|
+
|
|
2504
|
+
export const validateFleetCutoverHeldVolumeFenceEvidencePage = async (
|
|
2505
|
+
valueArg: unknown,
|
|
2506
|
+
): Promise<string[]> => {
|
|
2507
|
+
try {
|
|
2508
|
+
const value = snapshotFleetCutoverHeldVolumeFenceEvidencePage(valueArg);
|
|
2509
|
+
if (new TextEncoder().encode(canonicalizeStrictJson(value, fail)).byteLength
|
|
2510
|
+
> fleetCutoverContract.maximumHeldVolumeFenceEvidenceBytes) {
|
|
2511
|
+
return ['fleet cutover held volume fence evidence page exceeds its encoded byte bound'];
|
|
2512
|
+
}
|
|
2513
|
+
if ((await validateFleetCutoverPreparedVolumeHandoffPage(value.page)).length) {
|
|
2514
|
+
return ['fleet cutover held volume fence evidence page is not a valid immutable page'];
|
|
2515
|
+
}
|
|
2516
|
+
for (const fence of value.fences) {
|
|
2517
|
+
if ((await validateFleetCutoverVolumeFence(fence)).length) {
|
|
2518
|
+
return ['fleet cutover held volume fence evidence contains an invalid held fence'];
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
return [];
|
|
2522
|
+
} catch {
|
|
2523
|
+
return ['fleet cutover held volume fence evidence page must use its exact canonical schema'];
|
|
2524
|
+
}
|
|
2525
|
+
};
|
|
2526
|
+
|
|
1376
2527
|
export const snapshotFleetCutoverCoreflowRetirementEvidence = (
|
|
1377
2528
|
valueArg: unknown,
|
|
1378
2529
|
): IFleetCutoverCoreflowRetirementEvidence => safe(() => {
|