@serve.zone/interfaces 32.15.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 +9 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/fleetcutover.d.ts +65 -0
- package/dist_ts/data/fleetcutover.js +215 -7
- package/package.json +1 -1
- package/readme.md +38 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/fleetcutover.ts +314 -5
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1056,6 +1056,24 @@ sequence, observation time and fixed `freshUntil`; assembling a later census can
|
|
|
1056
1056
|
observation. It never transports environment, arbitrary labels, mount paths, volume names,
|
|
1057
1057
|
registry credentials or secret values.
|
|
1058
1058
|
|
|
1059
|
+
`data.computeFleetCutoverDockerServiceSpecDigest` hashes the complete open-schema Docker
|
|
1060
|
+
`Service.Spec`, including response properties unknown to this Interfaces version, under
|
|
1061
|
+
`serve.zone/fleet-cutover-docker-service-spec`. The helper returns only the digest: the full spec,
|
|
1062
|
+
including any environment values or secret references it contains, never belongs in the
|
|
1063
|
+
observation or logs. `data.computeFleetCutoverDockerConstraintSetIdentity` sorts exact constraint
|
|
1064
|
+
strings in UTF-16 code-unit order, preserves duplicates, and binds both the resulting list and its
|
|
1065
|
+
count. `data.snapshotFleetCutoverDockerDesiredTaskCount` admits only the non-negative safe integer
|
|
1066
|
+
from `ServiceStatus.DesiredTasks`; the producer must request service status and must not derive a
|
|
1067
|
+
replacement from task state. These helpers consume only the Docker response fields they name, so
|
|
1068
|
+
additional response properties do not make a newer daemon unreadable.
|
|
1069
|
+
|
|
1070
|
+
The only established historical service-identity ownership label is
|
|
1071
|
+
`data.fleetCutoverDockerLabelNames.serviceId`, whose exact value is `serve.zone.serviceId`.
|
|
1072
|
+
`ownershipLabels.platformOwner: null` means there is no known Docker-label evidence of a platform
|
|
1073
|
+
owner. It does not classify the service as an application, and neither a service name nor an
|
|
1074
|
+
unrecognized label supplies that classification. Cloudly's accepted administrator classification
|
|
1075
|
+
remains the authority for unlabeled legacy platform services.
|
|
1076
|
+
|
|
1059
1077
|
Mount identity has three explicit canonical domains. `serve.zone/fleet-cutover-mount-source-identity`
|
|
1060
1078
|
hashes `{ type, source }`, where `source` is the exact UTF-8 `Mount.Source` returned by Docker,
|
|
1061
1079
|
including an empty string. `serve.zone/fleet-cutover-mount-target-identity` hashes `{ target }`,
|
|
@@ -1068,6 +1086,26 @@ ownership also prevents completion rather than omitting the container. Structura
|
|
|
1068
1086
|
not establish freshness, authentication, manager consensus, ownership or Docker truth; Cloudly
|
|
1069
1087
|
establishes those policy facts while dereferencing the accepted observations.
|
|
1070
1088
|
|
|
1089
|
+
Docker exposes two different volume sources. A service-spec `Mount.Source` is the logical volume
|
|
1090
|
+
name, while a local container `MountPoint.Source` is its physical host path and `MountPoint.Name`
|
|
1091
|
+
is the logical name. `data.createFleetCutoverDockerServiceMountDescriptor` and
|
|
1092
|
+
`data.createFleetCutoverDockerLocalMountDescriptor` therefore keep their exact raw-source mount
|
|
1093
|
+
identities distinct. They never substitute `Name` for `Source`. The local-only
|
|
1094
|
+
`data.createFleetCutoverDockerPhysicalMountEvidence` adds a value-free correlation from the
|
|
1095
|
+
physical `mountIdentityDigest` to the service-spec identity derived from exact `MountPoint.Name`
|
|
1096
|
+
for volumes, or exact `Source` for bind and tmpfs mounts. Its independently domain-separated
|
|
1097
|
+
correlation digest binds that pair. When a physical container states `mountCorrelations`, the list
|
|
1098
|
+
must be sorted, unique, bounded, and a complete one-to-one mapping of its mounts. The property is
|
|
1099
|
+
optional only so observations created before this evidence existed still parse; absence proves no
|
|
1100
|
+
join, and a cutover consumer requiring a physical/service match must refuse it. Unsupported local
|
|
1101
|
+
mount types and volume mounts without a non-empty `Name` cannot produce correlation evidence.
|
|
1102
|
+
|
|
1103
|
+
The shared exact classifier recognizes only a writable
|
|
1104
|
+
`/var/run/docker.sock` to `/var/run/docker.sock` bind as `docker-socket`, a read-only
|
|
1105
|
+
`/sys/fs/cgroup` to `/host/cgroup` bind as `cgroup`, every volume as `persistent-volume`, other
|
|
1106
|
+
binds as `bind`, and tmpfs as `tmpfs`. Other Docker types remain `unknown`; no path normalization
|
|
1107
|
+
or service-name inference is performed.
|
|
1108
|
+
|
|
1071
1109
|
`data.IFleetCutoverScope` is the server-owned authoritative service set. It starts `incomplete`;
|
|
1072
1110
|
every `complete` entry binds an exact Docker service id to the current service authority and an
|
|
1073
1111
|
accepted classification authority. `unknown` cannot appear in a complete structural snapshot.
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/data/fleetcutover.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { normalizeStorageRequest } from '../platform/storage.js';
|
|
|
19
19
|
import type { ISecretVersionReference } from './secret.js';
|
|
20
20
|
import {
|
|
21
21
|
canonicalizeStrictJson,
|
|
22
|
+
compareWireTokens,
|
|
22
23
|
createCanonicalJsonSha256Hex,
|
|
23
24
|
} from '../private/canonicaljson.js';
|
|
24
25
|
import {
|
|
@@ -82,6 +83,37 @@ export interface IFleetCutoverMountTargetIdentityInput {
|
|
|
82
83
|
target: string;
|
|
83
84
|
}
|
|
84
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
|
+
|
|
85
117
|
export type TFleetCutoverDockerTaskState =
|
|
86
118
|
| 'new'
|
|
87
119
|
| 'allocated'
|
|
@@ -148,6 +180,11 @@ export interface IFleetCutoverPhysicalContainer {
|
|
|
148
180
|
imageId: TSha256Digest;
|
|
149
181
|
serviceIdLabel: string | null;
|
|
150
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[];
|
|
151
188
|
}
|
|
152
189
|
|
|
153
190
|
export interface IFleetCutoverContainerSetIdentity {
|
|
@@ -636,6 +673,9 @@ export const fleetCutoverContract = Object.freeze({
|
|
|
636
673
|
maximumNodes: 1024,
|
|
637
674
|
maximumContainersPerNode: 512,
|
|
638
675
|
maximumMountsPerOwner: 32,
|
|
676
|
+
maximumConstraintsPerService: 256,
|
|
677
|
+
maximumDockerTextBytes: 4096,
|
|
678
|
+
maximumDockerServiceSpecCanonicalBytes: 1_048_576,
|
|
639
679
|
maximumPreparedVolumeHandoffMutationEntries: 32,
|
|
640
680
|
/** Every admitted task has one node placement and at most 32 mounts: 4096 * 32. */
|
|
641
681
|
maximumPreparedVolumeHandoffs: 4096 * 32,
|
|
@@ -646,6 +686,9 @@ export const fleetCutoverContract = Object.freeze({
|
|
|
646
686
|
mountSourceIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-source-identity',
|
|
647
687
|
mountTargetIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-target-identity',
|
|
648
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',
|
|
649
692
|
containerSetIdentityDigestDomain: 'serve.zone/fleet-cutover-container-set-identity',
|
|
650
693
|
localNodeSnapshotDigestDomain: 'serve.zone/fleet-cutover-local-node-snapshot',
|
|
651
694
|
managerSnapshotDigestDomain: 'serve.zone/fleet-cutover-manager-snapshot',
|
|
@@ -673,6 +716,11 @@ export const fleetCutoverContract = Object.freeze({
|
|
|
673
716
|
} satisfies Record<TClusterRuntimePhase, TClusterRuntimePhase | null>),
|
|
674
717
|
} as const);
|
|
675
718
|
|
|
719
|
+
/** Docker labels with established historical producer semantics. */
|
|
720
|
+
export const fleetCutoverDockerLabelNames = Object.freeze({
|
|
721
|
+
serviceId: 'serve.zone.serviceId',
|
|
722
|
+
} as const);
|
|
723
|
+
|
|
676
724
|
const fail = (): never => { throw new Error('Fleet cutover contract rejected.'); };
|
|
677
725
|
const safe = <T>(operationArg: () => T): T => {
|
|
678
726
|
try { return operationArg(); } catch { return fail(); }
|
|
@@ -764,6 +812,28 @@ const sortedMountDescriptors = (valueArg: unknown): IFleetCutoverMountDescriptor
|
|
|
764
812
|
networkList(valueArg, fleetCutoverContract.maximumMountsPerOwner, mountDescriptor),
|
|
765
813
|
itemArg => itemArg.mountIdentityDigest,
|
|
766
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
|
+
);
|
|
767
837
|
const dockerMode = (valueArg: unknown): TFleetCutoverDockerServiceMode => {
|
|
768
838
|
requireNetwork(valueArg !== null && typeof valueArg === 'object');
|
|
769
839
|
const kind = (valueArg as Record<string, unknown>).kind;
|
|
@@ -788,6 +858,8 @@ const dockerService = (valueArg: unknown): IFleetCutoverDockerService => {
|
|
|
788
858
|
'dockerServiceId', 'dockerServiceName', 'specVersion', 'specDigest', 'imageDigest', 'mode',
|
|
789
859
|
'desiredTaskCount', 'constraintCount', 'constraintSetDigest', 'mounts', 'ownershipLabels',
|
|
790
860
|
]);
|
|
861
|
+
const constraintCount = nonNegative(value.constraintCount);
|
|
862
|
+
requireNetwork(constraintCount <= fleetCutoverContract.maximumConstraintsPerService);
|
|
791
863
|
return {
|
|
792
864
|
dockerServiceId: networkId(value.dockerServiceId),
|
|
793
865
|
dockerServiceName: networkId(value.dockerServiceName),
|
|
@@ -796,7 +868,7 @@ const dockerService = (valueArg: unknown): IFleetCutoverDockerService => {
|
|
|
796
868
|
imageDigest: networkDigest(value.imageDigest),
|
|
797
869
|
mode: dockerMode(value.mode),
|
|
798
870
|
desiredTaskCount: nonNegative(value.desiredTaskCount),
|
|
799
|
-
constraintCount
|
|
871
|
+
constraintCount,
|
|
800
872
|
constraintSetDigest: networkDigest(value.constraintSetDigest),
|
|
801
873
|
mounts: sortedMountDescriptors(value.mounts),
|
|
802
874
|
ownershipLabels: ownershipLabels(value.ownershipLabels),
|
|
@@ -833,8 +905,8 @@ const dockerTask = (valueArg: unknown): IFleetCutoverDockerTask => {
|
|
|
833
905
|
const physicalContainer = (valueArg: unknown): IFleetCutoverPhysicalContainer => {
|
|
834
906
|
const value = networkRecord(valueArg, [
|
|
835
907
|
'dockerContainerId', 'dockerServiceId', 'dockerTaskId', 'imageId', 'serviceIdLabel', 'mounts',
|
|
836
|
-
]);
|
|
837
|
-
|
|
908
|
+
], ['mountCorrelations']);
|
|
909
|
+
const result: IFleetCutoverPhysicalContainer = {
|
|
838
910
|
dockerContainerId: networkId(value.dockerContainerId),
|
|
839
911
|
dockerServiceId: value.dockerServiceId === null ? null : networkId(value.dockerServiceId),
|
|
840
912
|
dockerTaskId: value.dockerTaskId === null ? null : networkId(value.dockerTaskId),
|
|
@@ -842,6 +914,10 @@ const physicalContainer = (valueArg: unknown): IFleetCutoverPhysicalContainer =>
|
|
|
842
914
|
serviceIdLabel: value.serviceIdLabel === null ? null : networkId(value.serviceIdLabel),
|
|
843
915
|
mounts: sortedMountDescriptors(value.mounts),
|
|
844
916
|
};
|
|
917
|
+
if (Object.hasOwn(value, 'mountCorrelations')) {
|
|
918
|
+
result.mountCorrelations = sortedPhysicalMountCorrelations(value.mountCorrelations);
|
|
919
|
+
}
|
|
920
|
+
return result;
|
|
845
921
|
};
|
|
846
922
|
const inventory = (valueArg: unknown): IFleetCutoverDockerInventory => {
|
|
847
923
|
const value = networkRecord(valueArg, [
|
|
@@ -925,11 +1001,17 @@ const canonicalDigest = async (domainArg: string, valueArg: object): Promise<TSh
|
|
|
925
1001
|
|
|
926
1002
|
const exactDockerText = (valueArg: unknown, allowEmptyArg: boolean): string => {
|
|
927
1003
|
requireNetwork(typeof valueArg === 'string'
|
|
928
|
-
&& new TextEncoder().encode(valueArg).length <=
|
|
1004
|
+
&& new TextEncoder().encode(valueArg).length <= fleetCutoverContract.maximumDockerTextBytes
|
|
929
1005
|
&& (allowEmptyArg || valueArg.length > 0));
|
|
930
1006
|
return valueArg;
|
|
931
1007
|
};
|
|
932
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
|
+
|
|
933
1015
|
const computeExactIdentityDigest = async (
|
|
934
1016
|
domainArg: string,
|
|
935
1017
|
inputArg: object,
|
|
@@ -938,6 +1020,160 @@ const computeExactIdentityDigest = async (
|
|
|
938
1020
|
input: inputArg,
|
|
939
1021
|
}, fail), fail)}`;
|
|
940
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
|
+
|
|
941
1177
|
export const computeFleetCutoverMountSourceIdentityDigest = async (
|
|
942
1178
|
valueArg: IFleetCutoverMountSourceIdentityInput,
|
|
943
1179
|
): Promise<TSha256Digest> => safe(async () => {
|
|
@@ -973,6 +1209,54 @@ export const computeFleetCutoverMountIdentityDigest = async (
|
|
|
973
1209
|
});
|
|
974
1210
|
});
|
|
975
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
|
+
|
|
976
1260
|
const validateMountDescriptors = async (
|
|
977
1261
|
mountsArg: IFleetCutoverMountDescriptor[],
|
|
978
1262
|
): Promise<void> => {
|
|
@@ -982,6 +1266,28 @@ const validateMountDescriptors = async (
|
|
|
982
1266
|
}
|
|
983
1267
|
};
|
|
984
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
|
+
|
|
985
1291
|
export const computeFleetCutoverContainerSetIdentityDigest = async (
|
|
986
1292
|
containerIdsArg: string[],
|
|
987
1293
|
): Promise<TSha256Digest> => safe(async () => {
|
|
@@ -1046,7 +1352,10 @@ export const validateFleetCutoverLocalNodeSnapshot = async (
|
|
|
1046
1352
|
);
|
|
1047
1353
|
requireNetwork(value.before.containerIdsDigest === containerIdsDigest
|
|
1048
1354
|
&& value.after.containerIdsDigest === containerIdsDigest);
|
|
1049
|
-
for (const container of value.containers)
|
|
1355
|
+
for (const container of value.containers) {
|
|
1356
|
+
await validateMountDescriptors(container.mounts);
|
|
1357
|
+
await validatePhysicalMountCorrelations(container);
|
|
1358
|
+
}
|
|
1050
1359
|
return value.snapshotDigest === await computeFleetCutoverLocalNodeSnapshotDigest(value)
|
|
1051
1360
|
? [] : ['fleet cutover local node snapshot digest does not match'];
|
|
1052
1361
|
} catch {
|