@serve.zone/interfaces 32.12.0 → 32.14.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 +31 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/clusternode.d.ts +11 -4
- package/dist_ts/data/clusternode.js +41 -6
- package/dist_ts/data/fleetcutover.d.ts +503 -0
- package/dist_ts/data/fleetcutover.js +1021 -0
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/requests/fleetcutover.d.ts +141 -0
- package/dist_ts/requests/fleetcutover.js +2 -0
- package/dist_ts/requests/index.d.ts +2 -1
- package/dist_ts/requests/index.js +3 -2
- package/package.json +1 -1
- package/readme.md +129 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/clusternode.ts +55 -5
- package/ts/data/fleetcutover.ts +1693 -0
- package/ts/data/index.ts +1 -0
- package/ts/requests/fleetcutover.ts +160 -0
- package/ts/requests/index.ts +2 -0
|
@@ -0,0 +1,1693 @@
|
|
|
1
|
+
import type { TSha256Digest } from './immutableimage.js';
|
|
2
|
+
import type { IClusterRuntimeReference, TClusterRuntimePhase } from './clusterruntime.js';
|
|
3
|
+
import { snapshotClusterRuntimeReference } from './clusterruntime.js';
|
|
4
|
+
import type {
|
|
5
|
+
IRuntimeAssignmentReference,
|
|
6
|
+
IRuntimeAuthorityReference,
|
|
7
|
+
} from './runtimeassignment.js';
|
|
8
|
+
import type { IRuntimeSessionBinding } from './runtimesession.js';
|
|
9
|
+
import type { ISecretVersionReference } from './secret.js';
|
|
10
|
+
import {
|
|
11
|
+
canonicalizeStrictJson,
|
|
12
|
+
createCanonicalJsonSha256Hex,
|
|
13
|
+
} from '../private/canonicaljson.js';
|
|
14
|
+
import {
|
|
15
|
+
captureNetwork,
|
|
16
|
+
networkCounter,
|
|
17
|
+
networkDigest,
|
|
18
|
+
networkId,
|
|
19
|
+
networkList,
|
|
20
|
+
networkNodeId,
|
|
21
|
+
networkRecord,
|
|
22
|
+
networkReference,
|
|
23
|
+
networkSorted,
|
|
24
|
+
requireNetwork,
|
|
25
|
+
} from '../private/networkcontract.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A server-owned accepted record reference. It contains no bearer, secret, or proof boolean.
|
|
29
|
+
* Consumers dereference it against the current owner record before applying policy.
|
|
30
|
+
*/
|
|
31
|
+
export type IFleetCutoverAcceptedEvidenceReference = IRuntimeAuthorityReference;
|
|
32
|
+
|
|
33
|
+
export type TFleetCutoverPlatformOwner =
|
|
34
|
+
| 'application'
|
|
35
|
+
| 'coreflow'
|
|
36
|
+
| 'corestore'
|
|
37
|
+
| 'coretraffic';
|
|
38
|
+
|
|
39
|
+
export type TFleetCutoverMountClass =
|
|
40
|
+
| 'docker-socket'
|
|
41
|
+
| 'cgroup'
|
|
42
|
+
| 'persistent-volume'
|
|
43
|
+
| 'bind'
|
|
44
|
+
| 'tmpfs'
|
|
45
|
+
| 'other'
|
|
46
|
+
| 'unknown';
|
|
47
|
+
|
|
48
|
+
export type TFleetCutoverMountType = 'volume' | 'bind' | 'tmpfs' | 'unknown';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Value-free mount identity. The three digests are derived from exact Docker Engine bytes by the
|
|
52
|
+
* helpers below; no path, volume-name, case, separator, or alias normalization is permitted.
|
|
53
|
+
*/
|
|
54
|
+
export interface IFleetCutoverMountDescriptor {
|
|
55
|
+
type: TFleetCutoverMountType;
|
|
56
|
+
classification: TFleetCutoverMountClass;
|
|
57
|
+
sourceIdentityDigest: TSha256Digest;
|
|
58
|
+
targetIdentityDigest: TSha256Digest;
|
|
59
|
+
readOnly: boolean;
|
|
60
|
+
mountIdentityDigest: TSha256Digest;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface IFleetCutoverMountSourceIdentityInput {
|
|
64
|
+
/** Docker Engine Mount.Type exactly as returned; unsupported values are represented as unknown. */
|
|
65
|
+
type: TFleetCutoverMountType;
|
|
66
|
+
/** Docker Engine Mount.Source exact UTF-8 bytes, including the empty string; never normalize. */
|
|
67
|
+
source: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface IFleetCutoverMountTargetIdentityInput {
|
|
71
|
+
/** Docker Engine Mount.Destination exact UTF-8 bytes; never normalize. */
|
|
72
|
+
target: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type TFleetCutoverDockerTaskState =
|
|
76
|
+
| 'new'
|
|
77
|
+
| 'allocated'
|
|
78
|
+
| 'pending'
|
|
79
|
+
| 'assigned'
|
|
80
|
+
| 'accepted'
|
|
81
|
+
| 'preparing'
|
|
82
|
+
| 'ready'
|
|
83
|
+
| 'starting'
|
|
84
|
+
| 'running'
|
|
85
|
+
| 'complete'
|
|
86
|
+
| 'shutdown'
|
|
87
|
+
| 'failed'
|
|
88
|
+
| 'rejected'
|
|
89
|
+
| 'remove'
|
|
90
|
+
| 'orphaned';
|
|
91
|
+
|
|
92
|
+
export type TFleetCutoverDockerDesiredState = TFleetCutoverDockerTaskState;
|
|
93
|
+
|
|
94
|
+
export type TFleetCutoverDockerServiceMode =
|
|
95
|
+
| { kind: 'global' }
|
|
96
|
+
| { kind: 'replicated'; replicas: number };
|
|
97
|
+
|
|
98
|
+
/** Only the ownership labels used by cutover policy are transported. */
|
|
99
|
+
export interface IFleetCutoverDockerOwnershipLabels {
|
|
100
|
+
serviceId: string | null;
|
|
101
|
+
platformOwner: Exclude<TFleetCutoverPlatformOwner, 'application'> | null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Value-free immutable identity of one Docker service specification. */
|
|
105
|
+
export interface IFleetCutoverDockerService {
|
|
106
|
+
dockerServiceId: string;
|
|
107
|
+
dockerServiceName: string;
|
|
108
|
+
specVersion: number;
|
|
109
|
+
specDigest: TSha256Digest;
|
|
110
|
+
imageDigest: TSha256Digest;
|
|
111
|
+
mode: TFleetCutoverDockerServiceMode;
|
|
112
|
+
desiredTaskCount: number;
|
|
113
|
+
constraintCount: number;
|
|
114
|
+
constraintSetDigest: TSha256Digest;
|
|
115
|
+
mounts: IFleetCutoverMountDescriptor[];
|
|
116
|
+
ownershipLabels: IFleetCutoverDockerOwnershipLabels;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Value-free physical task/container evidence from the same Docker census. */
|
|
120
|
+
export interface IFleetCutoverDockerTask {
|
|
121
|
+
dockerTaskId: string;
|
|
122
|
+
dockerServiceId: string;
|
|
123
|
+
/** Null exactly when Docker Task.NodeID is absent and the task remains unassigned. */
|
|
124
|
+
swarmNodeId: string | null;
|
|
125
|
+
/** Resolved from the same manager census as swarmNodeId; null iff swarmNodeId is null. */
|
|
126
|
+
nodeName: string | null;
|
|
127
|
+
desiredState: TFleetCutoverDockerDesiredState;
|
|
128
|
+
state: TFleetCutoverDockerTaskState;
|
|
129
|
+
container: null | {
|
|
130
|
+
dockerContainerId: string;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface IFleetCutoverPhysicalContainer {
|
|
135
|
+
dockerContainerId: string;
|
|
136
|
+
dockerServiceId: string | null;
|
|
137
|
+
dockerTaskId: string | null;
|
|
138
|
+
imageId: TSha256Digest;
|
|
139
|
+
serviceIdLabel: string | null;
|
|
140
|
+
mounts: IFleetCutoverMountDescriptor[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface IFleetCutoverContainerSetIdentity {
|
|
144
|
+
containerCount: number;
|
|
145
|
+
containerIdsDigest: TSha256Digest;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* One node's local Docker daemon snapshot. `before` and `after` are independently read bounded
|
|
150
|
+
* container-ID sets around all inspect calls and must equal the transported container list.
|
|
151
|
+
*/
|
|
152
|
+
export interface IFleetCutoverLocalNodeSnapshot {
|
|
153
|
+
swarmNodeId: string;
|
|
154
|
+
before: IFleetCutoverContainerSetIdentity;
|
|
155
|
+
containers: IFleetCutoverPhysicalContainer[];
|
|
156
|
+
after: IFleetCutoverContainerSetIdentity;
|
|
157
|
+
snapshotDigest: TSha256Digest;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Manager-visible Swarm state only; physical containers come from each node's own observation. */
|
|
161
|
+
export interface IFleetCutoverDockerInventory {
|
|
162
|
+
swarmClusterId: string;
|
|
163
|
+
managerSwarmNodeId: string;
|
|
164
|
+
services: IFleetCutoverDockerService[];
|
|
165
|
+
tasks: IFleetCutoverDockerTask[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface IFleetCutoverManagerSnapshot {
|
|
169
|
+
inventory: IFleetCutoverDockerInventory;
|
|
170
|
+
inventoryDigest: TSha256Digest;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Cloudly-owned reference to one already authenticated and accepted Spark observation. */
|
|
174
|
+
export interface IFleetCutoverSparkObservationReference {
|
|
175
|
+
observation: IFleetCutoverAcceptedEvidenceReference;
|
|
176
|
+
nodeId: string;
|
|
177
|
+
swarmNodeId: string;
|
|
178
|
+
credentialGeneration: number;
|
|
179
|
+
reporterSessionId: string;
|
|
180
|
+
observationSequence: number;
|
|
181
|
+
observedAt: number;
|
|
182
|
+
acceptedAt: number;
|
|
183
|
+
freshUntil: number;
|
|
184
|
+
observationDigest: TSha256Digest;
|
|
185
|
+
snapshotDigest: TSha256Digest;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface IFleetCutoverManagerCensusRead
|
|
189
|
+
extends IFleetCutoverSparkObservationReference {
|
|
190
|
+
swarmSnapshotDigest: TSha256Digest;
|
|
191
|
+
inventoryDigest: TSha256Digest;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface IFleetCutoverTargetSetReference {
|
|
195
|
+
cloudlyClusterId: string;
|
|
196
|
+
generation: number;
|
|
197
|
+
targetSetDigest: TSha256Digest;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Cloudly-assembled candidate over accepted Spark observations; Spark never submits this shape. */
|
|
201
|
+
export interface IFleetCutoverCensusCandidate {
|
|
202
|
+
clusterId: string;
|
|
203
|
+
runtime: IClusterRuntimeReference;
|
|
204
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
205
|
+
inventory: IFleetCutoverDockerInventory;
|
|
206
|
+
/** Exactly two consecutive accepted reads by the same current manager session. */
|
|
207
|
+
managerReads: [IFleetCutoverManagerCensusRead, IFleetCutoverManagerCensusRead];
|
|
208
|
+
/** Exactly one accepted local physical-container observation per current target node. */
|
|
209
|
+
nodeObservations: IFleetCutoverSparkObservationReference[];
|
|
210
|
+
/** Fixed by the oldest referenced observation; reassembly cannot extend it. */
|
|
211
|
+
freshUntil: number;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Cloudly-owned accepted census. Acceptance freshness and authentication are owner policy. */
|
|
215
|
+
export interface IFleetCutoverCensus extends IFleetCutoverCensusCandidate {
|
|
216
|
+
id: string;
|
|
217
|
+
generation: number;
|
|
218
|
+
acceptedAt: number;
|
|
219
|
+
digest: TSha256Digest;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface IFleetCutoverScopeEntry {
|
|
223
|
+
dockerServiceId: string;
|
|
224
|
+
serviceId: string | null;
|
|
225
|
+
owner: TFleetCutoverPlatformOwner | 'unknown';
|
|
226
|
+
serviceAuthority: IFleetCutoverAcceptedEvidenceReference | null;
|
|
227
|
+
classificationAuthority: IFleetCutoverAcceptedEvidenceReference | null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Authoritative cutover scope. `complete` is a Cloudly policy verdict over current service
|
|
232
|
+
* authority and a fresh accepted census; structural validation only checks its closed content.
|
|
233
|
+
*/
|
|
234
|
+
export interface IFleetCutoverScope {
|
|
235
|
+
id: string;
|
|
236
|
+
generation: number;
|
|
237
|
+
previous: IFleetCutoverAcceptedEvidenceReference | null;
|
|
238
|
+
state: 'incomplete' | 'complete';
|
|
239
|
+
clusterId: string;
|
|
240
|
+
census: IFleetCutoverAcceptedEvidenceReference;
|
|
241
|
+
runtime: IClusterRuntimeReference;
|
|
242
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
243
|
+
entries: IFleetCutoverScopeEntry[];
|
|
244
|
+
actorId: string;
|
|
245
|
+
acceptedAt: number;
|
|
246
|
+
digest: TSha256Digest;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Legitimate administrator classification bound to one current accepted census. */
|
|
250
|
+
export interface IFleetCutoverClassification {
|
|
251
|
+
dockerServiceId: string;
|
|
252
|
+
serviceId: string;
|
|
253
|
+
owner: TFleetCutoverPlatformOwner;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface IFleetCutoverScopeClassificationRequest {
|
|
257
|
+
mutationId: string;
|
|
258
|
+
clusterId: string;
|
|
259
|
+
expectedScopeGeneration: number;
|
|
260
|
+
census: IFleetCutoverAcceptedEvidenceReference;
|
|
261
|
+
classifications: IFleetCutoverClassification[];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Server-owned pre-switch intent. It is not a ready storage claim or mount authority. */
|
|
265
|
+
export interface IFleetCutoverVolumeHandoffIntent {
|
|
266
|
+
id: string;
|
|
267
|
+
generation: number;
|
|
268
|
+
clusterId: string;
|
|
269
|
+
nodeId: string;
|
|
270
|
+
serviceId: string;
|
|
271
|
+
volumeId: string;
|
|
272
|
+
mountIdentityDigest: TSha256Digest;
|
|
273
|
+
sourceLineageDigest: TSha256Digest;
|
|
274
|
+
storageRequestDigest: TSha256Digest;
|
|
275
|
+
runtime: IClusterRuntimeReference & { phase: 'draining' };
|
|
276
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
277
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
278
|
+
legacyStorageEvidence: IFleetCutoverAcceptedEvidenceReference;
|
|
279
|
+
sourceNodeObservation: IFleetCutoverAcceptedEvidenceReference;
|
|
280
|
+
acceptedAt: number;
|
|
281
|
+
digest: TSha256Digest;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
interface IFleetCutoverVolumeFenceBase {
|
|
285
|
+
id: string;
|
|
286
|
+
generation: number;
|
|
287
|
+
previous: IFleetCutoverAcceptedEvidenceReference | null;
|
|
288
|
+
clusterId: string;
|
|
289
|
+
nodeId: string;
|
|
290
|
+
serviceId: string;
|
|
291
|
+
volumeId: string;
|
|
292
|
+
mountIdentityDigest: TSha256Digest;
|
|
293
|
+
handoffIntent: IFleetCutoverAcceptedEvidenceReference;
|
|
294
|
+
heldRuntime: IClusterRuntimeReference & { phase: 'draining' };
|
|
295
|
+
heldAt: number;
|
|
296
|
+
digest: TSha256Digest;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Pallet root-local durable negative authority. `held` grants no runtime session, assignment,
|
|
301
|
+
* attachment, reservation, confirmation, or execution authority.
|
|
302
|
+
*/
|
|
303
|
+
export type TFleetCutoverVolumeFence =
|
|
304
|
+
| (IFleetCutoverVolumeFenceBase & { state: 'held' })
|
|
305
|
+
| (IFleetCutoverVolumeFenceBase & {
|
|
306
|
+
state: 'consumed';
|
|
307
|
+
consumedRuntime: IClusterRuntimeReference & { phase: 'pallet' };
|
|
308
|
+
adoptionReceipt: IFleetCutoverAcceptedEvidenceReference;
|
|
309
|
+
claim: IRuntimeAuthorityReference;
|
|
310
|
+
consumedAt: number;
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
/** Corestore's one-shot accepted result around the initial lineage/RWO CAS. */
|
|
314
|
+
export interface IFleetCutoverCorestoreAdoptionReceipt {
|
|
315
|
+
id: string;
|
|
316
|
+
generation: 1;
|
|
317
|
+
clusterId: string;
|
|
318
|
+
nodeId: string;
|
|
319
|
+
serviceId: string;
|
|
320
|
+
volumeId: string;
|
|
321
|
+
mountIdentityDigest: TSha256Digest;
|
|
322
|
+
handoffIntent: IFleetCutoverAcceptedEvidenceReference;
|
|
323
|
+
palletFence: IFleetCutoverAcceptedEvidenceReference;
|
|
324
|
+
sourceLineageDigest: TSha256Digest;
|
|
325
|
+
claim: IRuntimeAuthorityReference;
|
|
326
|
+
accessMode: 'ReadWriteOnce';
|
|
327
|
+
acceptedAt: number;
|
|
328
|
+
digest: TSha256Digest;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export interface IFleetCutoverCoreflowRetirementEvidence {
|
|
332
|
+
id: string;
|
|
333
|
+
generation: number;
|
|
334
|
+
clusterId: string;
|
|
335
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
336
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
337
|
+
runtime: IClusterRuntimeReference;
|
|
338
|
+
relay: {
|
|
339
|
+
serviceId: string;
|
|
340
|
+
nodeId: string;
|
|
341
|
+
dockerServiceId: string;
|
|
342
|
+
dockerContainerId: string;
|
|
343
|
+
specVersion: number;
|
|
344
|
+
specDigest: TSha256Digest;
|
|
345
|
+
imageDigest: TSha256Digest;
|
|
346
|
+
managerObservation: IFleetCutoverAcceptedEvidenceReference;
|
|
347
|
+
nodeObservation: IFleetCutoverAcceptedEvidenceReference;
|
|
348
|
+
credential: IRuntimeAuthorityReference;
|
|
349
|
+
acceptedRegistration: IFleetCutoverAcceptedEvidenceReference;
|
|
350
|
+
version: string;
|
|
351
|
+
};
|
|
352
|
+
spark: {
|
|
353
|
+
nodeId: string;
|
|
354
|
+
credentialGeneration: number;
|
|
355
|
+
reporterSessionId: string;
|
|
356
|
+
observationSequence: number;
|
|
357
|
+
observation: IFleetCutoverAcceptedEvidenceReference;
|
|
358
|
+
bundleVersion: string;
|
|
359
|
+
bundleSourceCommit: string;
|
|
360
|
+
bundleManifestSha256: string;
|
|
361
|
+
unitName: string;
|
|
362
|
+
unitDigest: TSha256Digest;
|
|
363
|
+
};
|
|
364
|
+
legacy: {
|
|
365
|
+
credential: IRuntimeAuthorityReference;
|
|
366
|
+
invalidation: IFleetCutoverAcceptedEvidenceReference;
|
|
367
|
+
oldModeRefusal: IFleetCutoverAcceptedEvidenceReference;
|
|
368
|
+
configurationCas: IFleetCutoverAcceptedEvidenceReference;
|
|
369
|
+
sourceOwnerGeneration: number;
|
|
370
|
+
sourceSha256: string;
|
|
371
|
+
sourceDisposition: 'retained-inert' | 'removed';
|
|
372
|
+
};
|
|
373
|
+
acceptedAt: number;
|
|
374
|
+
digest: TSha256Digest;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Administrator intent to prepare the one explicitly designated ingress service. The caller
|
|
379
|
+
* supplies only compare-and-swap inputs; Cloudly owns credential generation and acceptance.
|
|
380
|
+
*/
|
|
381
|
+
export interface IFleetCutoverIngressPreparationRequest {
|
|
382
|
+
mutationId: string;
|
|
383
|
+
clusterId: string;
|
|
384
|
+
ingressServiceId: string;
|
|
385
|
+
currentRuntime: IClusterRuntimeReference & { phase: 'pallet' };
|
|
386
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
387
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
388
|
+
expectedSpecRevision: number;
|
|
389
|
+
/** Null for creation; the exact current designation for same-service credential rotation. */
|
|
390
|
+
expectedDesignation: IFleetCutoverAcceptedEvidenceReference | null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Public identity of the active ingress credential generation; it contains no credential value. */
|
|
394
|
+
export interface IFleetCutoverIngressCredentialReference {
|
|
395
|
+
credentialId: string;
|
|
396
|
+
generation: number;
|
|
397
|
+
secretVersion: ISecretVersionReference;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Cloudly's accepted pre-assignment designation. This grants no assignment, session,
|
|
402
|
+
* registration, publication, port-transfer, or post-registration ingress authority.
|
|
403
|
+
*/
|
|
404
|
+
export interface IFleetCutoverIngressDesignation {
|
|
405
|
+
id: string;
|
|
406
|
+
generation: number;
|
|
407
|
+
previous: IFleetCutoverAcceptedEvidenceReference | null;
|
|
408
|
+
clusterId: string;
|
|
409
|
+
runtime: IClusterRuntimeReference & { phase: 'pallet' };
|
|
410
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
411
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
412
|
+
ingressServiceId: string;
|
|
413
|
+
preparedSpecRevision: number;
|
|
414
|
+
credential: IFleetCutoverIngressCredentialReference;
|
|
415
|
+
actorId: string;
|
|
416
|
+
acceptedAt: number;
|
|
417
|
+
digest: TSha256Digest;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export interface IFleetCutoverIngressAuthority {
|
|
421
|
+
id: string;
|
|
422
|
+
generation: number;
|
|
423
|
+
clusterId: string;
|
|
424
|
+
runtime: IClusterRuntimeReference & { phase: 'pallet' };
|
|
425
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
426
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
427
|
+
ingressServiceId: string;
|
|
428
|
+
assignment: IRuntimeAssignmentReference;
|
|
429
|
+
session: IRuntimeSessionBinding;
|
|
430
|
+
ingressRegistration: IFleetCutoverAcceptedEvidenceReference;
|
|
431
|
+
registrationRouteRevision: number | null;
|
|
432
|
+
legacyIngressAbsence: IFleetCutoverAcceptedEvidenceReference;
|
|
433
|
+
sourcePublication: IFleetCutoverAcceptedEvidenceReference;
|
|
434
|
+
targetPublication: IFleetCutoverAcceptedEvidenceReference;
|
|
435
|
+
portTransfer: IFleetCutoverAcceptedEvidenceReference;
|
|
436
|
+
routeTableRevision: number;
|
|
437
|
+
routeTableReadback: IFleetCutoverAcceptedEvidenceReference;
|
|
438
|
+
acceptedAt: number;
|
|
439
|
+
digest: TSha256Digest;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
interface IFleetCutoverPhaseAdvanceBase {
|
|
443
|
+
mutationId: string;
|
|
444
|
+
clusterId: string;
|
|
445
|
+
currentRuntime: IClusterRuntimeReference;
|
|
446
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
447
|
+
targetSet: IFleetCutoverTargetSetReference;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Caller intent only. Cloudly dereferences every receipt and applies the transition atomically. */
|
|
451
|
+
export type TFleetCutoverPhaseAdvance =
|
|
452
|
+
| (IFleetCutoverPhaseAdvanceBase & {
|
|
453
|
+
from: 'swarm';
|
|
454
|
+
to: 'draining';
|
|
455
|
+
coreflowRetirement: IFleetCutoverAcceptedEvidenceReference;
|
|
456
|
+
})
|
|
457
|
+
| (IFleetCutoverPhaseAdvanceBase & {
|
|
458
|
+
from: 'draining';
|
|
459
|
+
to: 'switching';
|
|
460
|
+
swarmQuiescence: IFleetCutoverAcceptedEvidenceReference;
|
|
461
|
+
heldRuntimeSpecs: IFleetCutoverAcceptedEvidenceReference;
|
|
462
|
+
heldVolumeFences: IFleetCutoverAcceptedEvidenceReference;
|
|
463
|
+
})
|
|
464
|
+
| (IFleetCutoverPhaseAdvanceBase & {
|
|
465
|
+
from: 'switching';
|
|
466
|
+
to: 'pallet';
|
|
467
|
+
heldRuntimeSpecs: IFleetCutoverAcceptedEvidenceReference;
|
|
468
|
+
heldVolumeFences: IFleetCutoverAcceptedEvidenceReference;
|
|
469
|
+
palletPreparation: IFleetCutoverAcceptedEvidenceReference;
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
export type TFleetCutoverRuntimeSpecRelease =
|
|
473
|
+
| {
|
|
474
|
+
kind: 'ingress';
|
|
475
|
+
mutationId: string;
|
|
476
|
+
clusterId: string;
|
|
477
|
+
serviceId: string;
|
|
478
|
+
expectedSpecRevision: number;
|
|
479
|
+
runtime: IClusterRuntimeReference & { phase: 'pallet' };
|
|
480
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
481
|
+
emptyPublishedPortsPolicy: IFleetCutoverAcceptedEvidenceReference;
|
|
482
|
+
}
|
|
483
|
+
| {
|
|
484
|
+
kind: 'application';
|
|
485
|
+
mutationId: string;
|
|
486
|
+
clusterId: string;
|
|
487
|
+
serviceId: string;
|
|
488
|
+
expectedSpecRevision: number;
|
|
489
|
+
runtime: IClusterRuntimeReference & { phase: 'pallet' };
|
|
490
|
+
scope: IFleetCutoverAcceptedEvidenceReference;
|
|
491
|
+
ingressAuthority: IFleetCutoverAcceptedEvidenceReference;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
export const fleetCutoverContract = Object.freeze({
|
|
495
|
+
maximumServices: 512,
|
|
496
|
+
maximumTasks: 4096,
|
|
497
|
+
maximumNodes: 1024,
|
|
498
|
+
maximumContainersPerNode: 512,
|
|
499
|
+
maximumMountsPerOwner: 32,
|
|
500
|
+
maximumClassifications: 512,
|
|
501
|
+
observationFreshnessMs: 300_000,
|
|
502
|
+
mountSourceIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-source-identity',
|
|
503
|
+
mountTargetIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-target-identity',
|
|
504
|
+
mountIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-identity',
|
|
505
|
+
containerSetIdentityDigestDomain: 'serve.zone/fleet-cutover-container-set-identity',
|
|
506
|
+
localNodeSnapshotDigestDomain: 'serve.zone/fleet-cutover-local-node-snapshot',
|
|
507
|
+
managerSnapshotDigestDomain: 'serve.zone/fleet-cutover-manager-snapshot',
|
|
508
|
+
censusDigestDomain: 'serve.zone/fleet-cutover-census',
|
|
509
|
+
scopeDigestDomain: 'serve.zone/fleet-cutover-scope',
|
|
510
|
+
volumeHandoffDigestDomain: 'serve.zone/fleet-cutover-volume-handoff',
|
|
511
|
+
volumeFenceDigestDomain: 'serve.zone/fleet-cutover-volume-fence',
|
|
512
|
+
corestoreAdoptionDigestDomain: 'serve.zone/fleet-cutover-corestore-adoption',
|
|
513
|
+
coreflowRetirementDigestDomain: 'serve.zone/fleet-cutover-coreflow-retirement',
|
|
514
|
+
ingressDesignationDigestDomain: 'serve.zone/fleet-cutover-ingress-designation',
|
|
515
|
+
ingressAuthorityDigestDomain: 'serve.zone/fleet-cutover-ingress-authority',
|
|
516
|
+
legalPhaseTransitions: Object.freeze({
|
|
517
|
+
swarm: 'draining',
|
|
518
|
+
draining: 'switching',
|
|
519
|
+
switching: 'pallet',
|
|
520
|
+
pallet: null,
|
|
521
|
+
} satisfies Record<TClusterRuntimePhase, TClusterRuntimePhase | null>),
|
|
522
|
+
} as const);
|
|
523
|
+
|
|
524
|
+
const fail = (): never => { throw new Error('Fleet cutover contract rejected.'); };
|
|
525
|
+
const safe = <T>(operationArg: () => T): T => {
|
|
526
|
+
try { return operationArg(); } catch { return fail(); }
|
|
527
|
+
};
|
|
528
|
+
const nonNegative = (valueArg: unknown): number => {
|
|
529
|
+
requireNetwork(Number.isSafeInteger(valueArg) && (valueArg as number) >= 0);
|
|
530
|
+
return valueArg as number;
|
|
531
|
+
};
|
|
532
|
+
const literal = <T extends string>(valuesArg: readonly T[], valueArg: unknown): T => {
|
|
533
|
+
requireNetwork(valuesArg.includes(valueArg as T));
|
|
534
|
+
return valueArg as T;
|
|
535
|
+
};
|
|
536
|
+
const sha256Hex = (valueArg: unknown): string => {
|
|
537
|
+
requireNetwork(typeof valueArg === 'string' && /^[a-f0-9]{64}$/.test(valueArg));
|
|
538
|
+
return valueArg;
|
|
539
|
+
};
|
|
540
|
+
const releaseVersion = (valueArg: unknown): string => {
|
|
541
|
+
requireNetwork(typeof valueArg === 'string'
|
|
542
|
+
&& /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$/.test(valueArg)
|
|
543
|
+
&& valueArg.length <= 32);
|
|
544
|
+
return valueArg;
|
|
545
|
+
};
|
|
546
|
+
const reference = (valueArg: unknown): IFleetCutoverAcceptedEvidenceReference =>
|
|
547
|
+
networkReference(valueArg);
|
|
548
|
+
const targetSetReference = (valueArg: unknown): IFleetCutoverTargetSetReference => {
|
|
549
|
+
const value = networkRecord(valueArg, ['cloudlyClusterId', 'generation', 'targetSetDigest']);
|
|
550
|
+
return {
|
|
551
|
+
cloudlyClusterId: networkId(value.cloudlyClusterId),
|
|
552
|
+
generation: networkCounter(value.generation),
|
|
553
|
+
targetSetDigest: networkDigest(value.targetSetDigest),
|
|
554
|
+
};
|
|
555
|
+
};
|
|
556
|
+
const runtimeReference = (valueArg: unknown): IClusterRuntimeReference =>
|
|
557
|
+
snapshotClusterRuntimeReference(valueArg);
|
|
558
|
+
const assignmentReference = (valueArg: unknown): IRuntimeAssignmentReference => {
|
|
559
|
+
const value = networkRecord(valueArg, ['assignmentId', 'generation', 'digest']);
|
|
560
|
+
return {
|
|
561
|
+
assignmentId: networkId(value.assignmentId),
|
|
562
|
+
generation: networkCounter(value.generation),
|
|
563
|
+
digest: networkDigest(value.digest),
|
|
564
|
+
};
|
|
565
|
+
};
|
|
566
|
+
const ingressCredentialReference = (
|
|
567
|
+
valueArg: unknown,
|
|
568
|
+
): IFleetCutoverIngressCredentialReference => {
|
|
569
|
+
const value = networkRecord(valueArg, ['credentialId', 'generation', 'secretVersion']);
|
|
570
|
+
const secretVersion = networkRecord(value.secretVersion, ['secretId', 'secretVersionId']);
|
|
571
|
+
return {
|
|
572
|
+
credentialId: networkId(value.credentialId),
|
|
573
|
+
generation: networkCounter(value.generation),
|
|
574
|
+
secretVersion: {
|
|
575
|
+
secretId: networkId(secretVersion.secretId),
|
|
576
|
+
secretVersionId: networkId(secretVersion.secretVersionId),
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
};
|
|
580
|
+
const mountTypeAndClassification = (
|
|
581
|
+
typeArg: unknown,
|
|
582
|
+
classificationArg: unknown,
|
|
583
|
+
): Pick<IFleetCutoverMountDescriptor, 'type' | 'classification'> => {
|
|
584
|
+
const type = literal(['volume', 'bind', 'tmpfs', 'unknown'] as const, typeArg);
|
|
585
|
+
const classification = literal([
|
|
586
|
+
'docker-socket', 'cgroup', 'persistent-volume', 'bind', 'tmpfs', 'other', 'unknown',
|
|
587
|
+
] as const, classificationArg);
|
|
588
|
+
requireNetwork(
|
|
589
|
+
(type === 'volume' && ['persistent-volume', 'other', 'unknown'].includes(classification))
|
|
590
|
+
|| (type === 'bind' && ['docker-socket', 'cgroup', 'bind', 'other', 'unknown'].includes(classification))
|
|
591
|
+
|| (type === 'tmpfs' && ['tmpfs', 'other', 'unknown'].includes(classification))
|
|
592
|
+
|| (type === 'unknown' && classification === 'unknown'),
|
|
593
|
+
);
|
|
594
|
+
return { type, classification };
|
|
595
|
+
};
|
|
596
|
+
const mountDescriptor = (valueArg: unknown): IFleetCutoverMountDescriptor => {
|
|
597
|
+
const value = networkRecord(valueArg, [
|
|
598
|
+
'type', 'classification', 'sourceIdentityDigest', 'targetIdentityDigest', 'readOnly',
|
|
599
|
+
'mountIdentityDigest',
|
|
600
|
+
]);
|
|
601
|
+
requireNetwork(typeof value.readOnly === 'boolean');
|
|
602
|
+
const shape = mountTypeAndClassification(value.type, value.classification);
|
|
603
|
+
return {
|
|
604
|
+
...shape,
|
|
605
|
+
sourceIdentityDigest: networkDigest(value.sourceIdentityDigest),
|
|
606
|
+
targetIdentityDigest: networkDigest(value.targetIdentityDigest),
|
|
607
|
+
readOnly: value.readOnly,
|
|
608
|
+
mountIdentityDigest: networkDigest(value.mountIdentityDigest),
|
|
609
|
+
};
|
|
610
|
+
};
|
|
611
|
+
const sortedMountDescriptors = (valueArg: unknown): IFleetCutoverMountDescriptor[] => networkSorted(
|
|
612
|
+
networkList(valueArg, fleetCutoverContract.maximumMountsPerOwner, mountDescriptor),
|
|
613
|
+
itemArg => itemArg.mountIdentityDigest,
|
|
614
|
+
);
|
|
615
|
+
const dockerMode = (valueArg: unknown): TFleetCutoverDockerServiceMode => {
|
|
616
|
+
requireNetwork(valueArg !== null && typeof valueArg === 'object');
|
|
617
|
+
const kind = (valueArg as Record<string, unknown>).kind;
|
|
618
|
+
if (kind === 'global') {
|
|
619
|
+
networkRecord(valueArg, ['kind']);
|
|
620
|
+
return { kind };
|
|
621
|
+
}
|
|
622
|
+
const value = networkRecord(valueArg, ['kind', 'replicas']);
|
|
623
|
+
requireNetwork(kind === 'replicated');
|
|
624
|
+
return { kind, replicas: nonNegative(value.replicas) };
|
|
625
|
+
};
|
|
626
|
+
const ownershipLabels = (valueArg: unknown): IFleetCutoverDockerOwnershipLabels => {
|
|
627
|
+
const value = networkRecord(valueArg, ['serviceId', 'platformOwner']);
|
|
628
|
+
const serviceId = value.serviceId === null ? null : networkId(value.serviceId);
|
|
629
|
+
const platformOwner = value.platformOwner === null ? null : literal([
|
|
630
|
+
'coreflow', 'corestore', 'coretraffic',
|
|
631
|
+
] as const, value.platformOwner);
|
|
632
|
+
return { serviceId, platformOwner };
|
|
633
|
+
};
|
|
634
|
+
const dockerService = (valueArg: unknown): IFleetCutoverDockerService => {
|
|
635
|
+
const value = networkRecord(valueArg, [
|
|
636
|
+
'dockerServiceId', 'dockerServiceName', 'specVersion', 'specDigest', 'imageDigest', 'mode',
|
|
637
|
+
'desiredTaskCount', 'constraintCount', 'constraintSetDigest', 'mounts', 'ownershipLabels',
|
|
638
|
+
]);
|
|
639
|
+
return {
|
|
640
|
+
dockerServiceId: networkId(value.dockerServiceId),
|
|
641
|
+
dockerServiceName: networkId(value.dockerServiceName),
|
|
642
|
+
specVersion: networkCounter(value.specVersion),
|
|
643
|
+
specDigest: networkDigest(value.specDigest),
|
|
644
|
+
imageDigest: networkDigest(value.imageDigest),
|
|
645
|
+
mode: dockerMode(value.mode),
|
|
646
|
+
desiredTaskCount: nonNegative(value.desiredTaskCount),
|
|
647
|
+
constraintCount: nonNegative(value.constraintCount),
|
|
648
|
+
constraintSetDigest: networkDigest(value.constraintSetDigest),
|
|
649
|
+
mounts: sortedMountDescriptors(value.mounts),
|
|
650
|
+
ownershipLabels: ownershipLabels(value.ownershipLabels),
|
|
651
|
+
};
|
|
652
|
+
};
|
|
653
|
+
const dockerTask = (valueArg: unknown): IFleetCutoverDockerTask => {
|
|
654
|
+
const value = networkRecord(valueArg, [
|
|
655
|
+
'dockerTaskId', 'dockerServiceId', 'swarmNodeId', 'nodeName', 'desiredState', 'state', 'container',
|
|
656
|
+
]);
|
|
657
|
+
let container: IFleetCutoverDockerTask['container'] = null;
|
|
658
|
+
if (value.container !== null) {
|
|
659
|
+
const containerValue = networkRecord(value.container, ['dockerContainerId']);
|
|
660
|
+
container = {
|
|
661
|
+
dockerContainerId: networkId(containerValue.dockerContainerId),
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
const swarmNodeId = value.swarmNodeId === null ? null : networkId(value.swarmNodeId);
|
|
665
|
+
const nodeName = value.nodeName === null ? null : networkId(value.nodeName);
|
|
666
|
+
requireNetwork((swarmNodeId === null) === (nodeName === null));
|
|
667
|
+
const taskStates = [
|
|
668
|
+
'new', 'allocated', 'pending', 'assigned', 'accepted', 'preparing', 'ready', 'starting',
|
|
669
|
+
'running', 'complete', 'shutdown', 'failed', 'rejected', 'remove', 'orphaned',
|
|
670
|
+
] as const;
|
|
671
|
+
return {
|
|
672
|
+
dockerTaskId: networkId(value.dockerTaskId),
|
|
673
|
+
dockerServiceId: networkId(value.dockerServiceId),
|
|
674
|
+
swarmNodeId,
|
|
675
|
+
nodeName,
|
|
676
|
+
desiredState: literal(taskStates, value.desiredState),
|
|
677
|
+
state: literal(taskStates, value.state),
|
|
678
|
+
container,
|
|
679
|
+
};
|
|
680
|
+
};
|
|
681
|
+
const physicalContainer = (valueArg: unknown): IFleetCutoverPhysicalContainer => {
|
|
682
|
+
const value = networkRecord(valueArg, [
|
|
683
|
+
'dockerContainerId', 'dockerServiceId', 'dockerTaskId', 'imageId', 'serviceIdLabel', 'mounts',
|
|
684
|
+
]);
|
|
685
|
+
return {
|
|
686
|
+
dockerContainerId: networkId(value.dockerContainerId),
|
|
687
|
+
dockerServiceId: value.dockerServiceId === null ? null : networkId(value.dockerServiceId),
|
|
688
|
+
dockerTaskId: value.dockerTaskId === null ? null : networkId(value.dockerTaskId),
|
|
689
|
+
imageId: networkDigest(value.imageId),
|
|
690
|
+
serviceIdLabel: value.serviceIdLabel === null ? null : networkId(value.serviceIdLabel),
|
|
691
|
+
mounts: sortedMountDescriptors(value.mounts),
|
|
692
|
+
};
|
|
693
|
+
};
|
|
694
|
+
const inventory = (valueArg: unknown): IFleetCutoverDockerInventory => {
|
|
695
|
+
const value = networkRecord(valueArg, [
|
|
696
|
+
'swarmClusterId', 'managerSwarmNodeId', 'services', 'tasks',
|
|
697
|
+
]);
|
|
698
|
+
const services = networkSorted(networkList(
|
|
699
|
+
value.services,
|
|
700
|
+
fleetCutoverContract.maximumServices,
|
|
701
|
+
dockerService,
|
|
702
|
+
), itemArg => itemArg.dockerServiceId);
|
|
703
|
+
const serviceIds = new Set(services.map(itemArg => itemArg.dockerServiceId));
|
|
704
|
+
const tasks = networkSorted(networkList(
|
|
705
|
+
value.tasks,
|
|
706
|
+
fleetCutoverContract.maximumTasks,
|
|
707
|
+
dockerTask,
|
|
708
|
+
), itemArg => `${itemArg.dockerServiceId}\0${itemArg.dockerTaskId}`);
|
|
709
|
+
requireNetwork(tasks.every(itemArg => serviceIds.has(itemArg.dockerServiceId)));
|
|
710
|
+
return {
|
|
711
|
+
swarmClusterId: networkId(value.swarmClusterId),
|
|
712
|
+
managerSwarmNodeId: networkId(value.managerSwarmNodeId),
|
|
713
|
+
services,
|
|
714
|
+
tasks,
|
|
715
|
+
};
|
|
716
|
+
};
|
|
717
|
+
const observationReference = (valueArg: unknown): IFleetCutoverSparkObservationReference => {
|
|
718
|
+
const value = networkRecord(valueArg, [
|
|
719
|
+
'observation', 'nodeId', 'swarmNodeId', 'credentialGeneration', 'reporterSessionId',
|
|
720
|
+
'observationSequence', 'observedAt', 'acceptedAt', 'freshUntil', 'observationDigest',
|
|
721
|
+
'snapshotDigest',
|
|
722
|
+
]);
|
|
723
|
+
const result: IFleetCutoverSparkObservationReference = {
|
|
724
|
+
observation: reference(value.observation),
|
|
725
|
+
nodeId: networkNodeId(value.nodeId),
|
|
726
|
+
swarmNodeId: networkId(value.swarmNodeId),
|
|
727
|
+
credentialGeneration: networkCounter(value.credentialGeneration),
|
|
728
|
+
reporterSessionId: networkId(value.reporterSessionId),
|
|
729
|
+
observationSequence: networkCounter(value.observationSequence),
|
|
730
|
+
observedAt: networkCounter(value.observedAt),
|
|
731
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
732
|
+
freshUntil: networkCounter(value.freshUntil),
|
|
733
|
+
observationDigest: networkDigest(value.observationDigest),
|
|
734
|
+
snapshotDigest: networkDigest(value.snapshotDigest),
|
|
735
|
+
};
|
|
736
|
+
requireNetwork(result.acceptedAt >= result.observedAt
|
|
737
|
+
&& result.acceptedAt <= result.freshUntil
|
|
738
|
+
&& result.freshUntil === result.observedAt + fleetCutoverContract.observationFreshnessMs);
|
|
739
|
+
return result;
|
|
740
|
+
};
|
|
741
|
+
const managerCensusRead = (valueArg: unknown): IFleetCutoverManagerCensusRead => {
|
|
742
|
+
const value = networkRecord(valueArg, [
|
|
743
|
+
'observation', 'nodeId', 'swarmNodeId', 'credentialGeneration', 'reporterSessionId',
|
|
744
|
+
'observationSequence', 'observedAt', 'acceptedAt', 'freshUntil', 'observationDigest',
|
|
745
|
+
'snapshotDigest', 'swarmSnapshotDigest', 'inventoryDigest',
|
|
746
|
+
]);
|
|
747
|
+
return {
|
|
748
|
+
...observationReference({
|
|
749
|
+
observation: value.observation,
|
|
750
|
+
nodeId: value.nodeId,
|
|
751
|
+
swarmNodeId: value.swarmNodeId,
|
|
752
|
+
credentialGeneration: value.credentialGeneration,
|
|
753
|
+
reporterSessionId: value.reporterSessionId,
|
|
754
|
+
observationSequence: value.observationSequence,
|
|
755
|
+
observedAt: value.observedAt,
|
|
756
|
+
acceptedAt: value.acceptedAt,
|
|
757
|
+
freshUntil: value.freshUntil,
|
|
758
|
+
observationDigest: value.observationDigest,
|
|
759
|
+
snapshotDigest: value.snapshotDigest,
|
|
760
|
+
}),
|
|
761
|
+
swarmSnapshotDigest: networkDigest(value.swarmSnapshotDigest),
|
|
762
|
+
inventoryDigest: networkDigest(value.inventoryDigest),
|
|
763
|
+
};
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
const canonicalDigest = async (domainArg: string, valueArg: object): Promise<TSha256Digest> => {
|
|
767
|
+
const { digest: _digest, ...payload } = valueArg as { digest?: unknown };
|
|
768
|
+
return `sha256:${await createCanonicalJsonSha256Hex(canonicalizeStrictJson({
|
|
769
|
+
domain: domainArg,
|
|
770
|
+
payload,
|
|
771
|
+
}, fail), fail)}`;
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
const exactDockerText = (valueArg: unknown, allowEmptyArg: boolean): string => {
|
|
775
|
+
requireNetwork(typeof valueArg === 'string'
|
|
776
|
+
&& new TextEncoder().encode(valueArg).length <= 4096
|
|
777
|
+
&& (allowEmptyArg || valueArg.length > 0));
|
|
778
|
+
return valueArg;
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
const computeExactIdentityDigest = async (
|
|
782
|
+
domainArg: string,
|
|
783
|
+
inputArg: object,
|
|
784
|
+
): Promise<TSha256Digest> => `sha256:${await createCanonicalJsonSha256Hex(canonicalizeStrictJson({
|
|
785
|
+
domain: domainArg,
|
|
786
|
+
input: inputArg,
|
|
787
|
+
}, fail), fail)}`;
|
|
788
|
+
|
|
789
|
+
export const computeFleetCutoverMountSourceIdentityDigest = async (
|
|
790
|
+
valueArg: IFleetCutoverMountSourceIdentityInput,
|
|
791
|
+
): Promise<TSha256Digest> => safe(async () => {
|
|
792
|
+
const value = networkRecord(valueArg, ['type', 'source']);
|
|
793
|
+
return computeExactIdentityDigest(fleetCutoverContract.mountSourceIdentityDigestDomain, {
|
|
794
|
+
type: literal(['volume', 'bind', 'tmpfs', 'unknown'] as const, value.type),
|
|
795
|
+
source: exactDockerText(value.source, true),
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
export const computeFleetCutoverMountTargetIdentityDigest = async (
|
|
800
|
+
valueArg: IFleetCutoverMountTargetIdentityInput,
|
|
801
|
+
): Promise<TSha256Digest> => safe(async () => {
|
|
802
|
+
const value = networkRecord(valueArg, ['target']);
|
|
803
|
+
return computeExactIdentityDigest(fleetCutoverContract.mountTargetIdentityDigestDomain, {
|
|
804
|
+
target: exactDockerText(value.target, false),
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
export const computeFleetCutoverMountIdentityDigest = async (
|
|
809
|
+
valueArg: Omit<IFleetCutoverMountDescriptor, 'mountIdentityDigest'>,
|
|
810
|
+
): Promise<TSha256Digest> => safe(async () => {
|
|
811
|
+
const value = networkRecord(valueArg, [
|
|
812
|
+
'type', 'classification', 'sourceIdentityDigest', 'targetIdentityDigest', 'readOnly',
|
|
813
|
+
]);
|
|
814
|
+
requireNetwork(typeof value.readOnly === 'boolean');
|
|
815
|
+
const shape = mountTypeAndClassification(value.type, value.classification);
|
|
816
|
+
return computeExactIdentityDigest(fleetCutoverContract.mountIdentityDigestDomain, {
|
|
817
|
+
...shape,
|
|
818
|
+
sourceIdentityDigest: networkDigest(value.sourceIdentityDigest),
|
|
819
|
+
targetIdentityDigest: networkDigest(value.targetIdentityDigest),
|
|
820
|
+
readOnly: value.readOnly,
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const validateMountDescriptors = async (
|
|
825
|
+
mountsArg: IFleetCutoverMountDescriptor[],
|
|
826
|
+
): Promise<void> => {
|
|
827
|
+
for (const mount of mountsArg) {
|
|
828
|
+
const { mountIdentityDigest, ...input } = mount;
|
|
829
|
+
requireNetwork(mountIdentityDigest === await computeFleetCutoverMountIdentityDigest(input));
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
export const computeFleetCutoverContainerSetIdentityDigest = async (
|
|
834
|
+
containerIdsArg: string[],
|
|
835
|
+
): Promise<TSha256Digest> => safe(async () => {
|
|
836
|
+
const containerIds = networkSorted(networkList(
|
|
837
|
+
containerIdsArg,
|
|
838
|
+
fleetCutoverContract.maximumContainersPerNode,
|
|
839
|
+
networkId,
|
|
840
|
+
), itemArg => itemArg);
|
|
841
|
+
return computeExactIdentityDigest(fleetCutoverContract.containerSetIdentityDigestDomain, {
|
|
842
|
+
containerIds,
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
const containerSetIdentity = (valueArg: unknown): IFleetCutoverContainerSetIdentity => {
|
|
847
|
+
const value = networkRecord(valueArg, ['containerCount', 'containerIdsDigest']);
|
|
848
|
+
return {
|
|
849
|
+
containerCount: nonNegative(value.containerCount),
|
|
850
|
+
containerIdsDigest: networkDigest(value.containerIdsDigest),
|
|
851
|
+
};
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
export const snapshotFleetCutoverLocalNodeSnapshot = (
|
|
855
|
+
valueArg: unknown,
|
|
856
|
+
): IFleetCutoverLocalNodeSnapshot => safe(() => {
|
|
857
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
858
|
+
'swarmNodeId', 'before', 'containers', 'after', 'snapshotDigest',
|
|
859
|
+
]);
|
|
860
|
+
const containers = networkSorted(networkList(
|
|
861
|
+
value.containers,
|
|
862
|
+
fleetCutoverContract.maximumContainersPerNode,
|
|
863
|
+
physicalContainer,
|
|
864
|
+
), itemArg => itemArg.dockerContainerId);
|
|
865
|
+
const before = containerSetIdentity(value.before);
|
|
866
|
+
const after = containerSetIdentity(value.after);
|
|
867
|
+
requireNetwork(before.containerCount === containers.length
|
|
868
|
+
&& after.containerCount === containers.length
|
|
869
|
+
&& before.containerIdsDigest === after.containerIdsDigest);
|
|
870
|
+
return {
|
|
871
|
+
swarmNodeId: networkId(value.swarmNodeId),
|
|
872
|
+
before,
|
|
873
|
+
containers,
|
|
874
|
+
after,
|
|
875
|
+
snapshotDigest: networkDigest(value.snapshotDigest),
|
|
876
|
+
};
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
export const computeFleetCutoverLocalNodeSnapshotDigest = async (
|
|
880
|
+
valueArg: unknown,
|
|
881
|
+
): Promise<TSha256Digest> => safe(async () => {
|
|
882
|
+
const value = snapshotFleetCutoverLocalNodeSnapshot(valueArg);
|
|
883
|
+
const { snapshotDigest: _omitted, ...input } = value;
|
|
884
|
+
return computeExactIdentityDigest(fleetCutoverContract.localNodeSnapshotDigestDomain, input);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
export const validateFleetCutoverLocalNodeSnapshot = async (
|
|
888
|
+
valueArg: unknown,
|
|
889
|
+
): Promise<string[]> => {
|
|
890
|
+
try {
|
|
891
|
+
const value = snapshotFleetCutoverLocalNodeSnapshot(valueArg);
|
|
892
|
+
const containerIdsDigest = await computeFleetCutoverContainerSetIdentityDigest(
|
|
893
|
+
value.containers.map(itemArg => itemArg.dockerContainerId),
|
|
894
|
+
);
|
|
895
|
+
requireNetwork(value.before.containerIdsDigest === containerIdsDigest
|
|
896
|
+
&& value.after.containerIdsDigest === containerIdsDigest);
|
|
897
|
+
for (const container of value.containers) await validateMountDescriptors(container.mounts);
|
|
898
|
+
return value.snapshotDigest === await computeFleetCutoverLocalNodeSnapshotDigest(value)
|
|
899
|
+
? [] : ['fleet cutover local node snapshot digest does not match'];
|
|
900
|
+
} catch {
|
|
901
|
+
return ['fleet cutover local node snapshot must use its exact canonical schema'];
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
export const computeFleetCutoverInventoryDigest = async (
|
|
906
|
+
valueArg: unknown,
|
|
907
|
+
): Promise<TSha256Digest> => safe(async () => {
|
|
908
|
+
const hex = await createCanonicalJsonSha256Hex(
|
|
909
|
+
canonicalizeStrictJson({
|
|
910
|
+
domain: 'serve.zone/fleet-cutover-inventory',
|
|
911
|
+
inventory: inventory(captureNetwork(valueArg)),
|
|
912
|
+
}, fail),
|
|
913
|
+
fail,
|
|
914
|
+
);
|
|
915
|
+
return `sha256:${hex}` as TSha256Digest;
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
export const snapshotFleetCutoverManagerSnapshot = async (
|
|
919
|
+
valueArg: unknown,
|
|
920
|
+
): Promise<IFleetCutoverManagerSnapshot> => safe(async () => {
|
|
921
|
+
const value = networkRecord(captureNetwork(valueArg), ['inventory', 'inventoryDigest']);
|
|
922
|
+
const inventoryValue = inventory(value.inventory);
|
|
923
|
+
const inventoryDigest = networkDigest(value.inventoryDigest);
|
|
924
|
+
requireNetwork(inventoryDigest === await computeFleetCutoverInventoryDigest(inventoryValue));
|
|
925
|
+
for (const service of inventoryValue.services) await validateMountDescriptors(service.mounts);
|
|
926
|
+
return { inventory: inventoryValue, inventoryDigest };
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
export const computeFleetCutoverManagerSnapshotDigest = async (
|
|
930
|
+
valueArg: unknown,
|
|
931
|
+
): Promise<TSha256Digest> => computeExactIdentityDigest(
|
|
932
|
+
fleetCutoverContract.managerSnapshotDigestDomain,
|
|
933
|
+
await snapshotFleetCutoverManagerSnapshot(valueArg),
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
export const validateFleetCutoverManagerSnapshot = async (
|
|
937
|
+
valueArg: unknown,
|
|
938
|
+
): Promise<string[]> => {
|
|
939
|
+
try {
|
|
940
|
+
await snapshotFleetCutoverManagerSnapshot(valueArg);
|
|
941
|
+
return [];
|
|
942
|
+
} catch {
|
|
943
|
+
return ['fleet cutover manager snapshot must use its exact canonical schema'];
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
export const snapshotFleetCutoverCensusCandidate = async (
|
|
948
|
+
valueArg: unknown,
|
|
949
|
+
): Promise<IFleetCutoverCensusCandidate> => safe(async () => {
|
|
950
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
951
|
+
'clusterId', 'runtime', 'targetSet', 'inventory', 'managerReads', 'nodeObservations',
|
|
952
|
+
'freshUntil',
|
|
953
|
+
]);
|
|
954
|
+
const clusterId = networkId(value.clusterId);
|
|
955
|
+
const runtime = runtimeReference(value.runtime);
|
|
956
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
957
|
+
const inventoryValue = inventory(value.inventory);
|
|
958
|
+
for (const service of inventoryValue.services) await validateMountDescriptors(service.mounts);
|
|
959
|
+
const managerReads = networkList(value.managerReads, 2, managerCensusRead);
|
|
960
|
+
const nodeObservations = networkSorted(networkList(
|
|
961
|
+
value.nodeObservations,
|
|
962
|
+
fleetCutoverContract.maximumNodes,
|
|
963
|
+
observationReference,
|
|
964
|
+
), itemArg => `${itemArg.nodeId}\0${itemArg.swarmNodeId}`);
|
|
965
|
+
requireNetwork(managerReads.length === 2
|
|
966
|
+
&& managerReads[1].nodeId === managerReads[0].nodeId
|
|
967
|
+
&& managerReads[1].swarmNodeId === managerReads[0].swarmNodeId
|
|
968
|
+
&& managerReads[1].credentialGeneration === managerReads[0].credentialGeneration
|
|
969
|
+
&& managerReads[1].reporterSessionId === managerReads[0].reporterSessionId
|
|
970
|
+
&& managerReads[1].observationSequence === managerReads[0].observationSequence + 1
|
|
971
|
+
&& managerReads[1].observedAt > managerReads[0].observedAt
|
|
972
|
+
&& managerReads[1].swarmSnapshotDigest === managerReads[0].swarmSnapshotDigest
|
|
973
|
+
&& managerReads.every(itemArg => itemArg.swarmNodeId === inventoryValue.managerSwarmNodeId)
|
|
974
|
+
&& nodeObservations.length > 0
|
|
975
|
+
&& new Set(nodeObservations.map(itemArg => itemArg.nodeId)).size === nodeObservations.length
|
|
976
|
+
&& new Set(nodeObservations.map(itemArg => itemArg.swarmNodeId)).size === nodeObservations.length
|
|
977
|
+
&& runtime.id === clusterId
|
|
978
|
+
&& targetSet.cloudlyClusterId === clusterId);
|
|
979
|
+
const inventoryDigest = await computeFleetCutoverInventoryDigest(inventoryValue);
|
|
980
|
+
const managerSnapshotDigest = await computeFleetCutoverManagerSnapshotDigest({
|
|
981
|
+
inventory: inventoryValue,
|
|
982
|
+
inventoryDigest,
|
|
983
|
+
});
|
|
984
|
+
requireNetwork(managerReads.every(itemArg => itemArg.inventoryDigest === inventoryDigest
|
|
985
|
+
&& itemArg.snapshotDigest === managerSnapshotDigest));
|
|
986
|
+
const freshUntil = networkCounter(value.freshUntil);
|
|
987
|
+
requireNetwork(freshUntil === Math.min(
|
|
988
|
+
...managerReads.map(itemArg => itemArg.freshUntil),
|
|
989
|
+
...nodeObservations.map(itemArg => itemArg.freshUntil),
|
|
990
|
+
));
|
|
991
|
+
return {
|
|
992
|
+
clusterId,
|
|
993
|
+
runtime,
|
|
994
|
+
targetSet,
|
|
995
|
+
inventory: inventoryValue,
|
|
996
|
+
managerReads: managerReads as [IFleetCutoverManagerCensusRead, IFleetCutoverManagerCensusRead],
|
|
997
|
+
nodeObservations,
|
|
998
|
+
freshUntil,
|
|
999
|
+
};
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
export const snapshotFleetCutoverCensus = async (
|
|
1003
|
+
valueArg: unknown,
|
|
1004
|
+
): Promise<IFleetCutoverCensus> => safe(async () => {
|
|
1005
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
1006
|
+
'id', 'generation', 'acceptedAt', 'digest', 'clusterId', 'runtime', 'targetSet',
|
|
1007
|
+
'inventory', 'managerReads', 'nodeObservations', 'freshUntil',
|
|
1008
|
+
]);
|
|
1009
|
+
const candidate = await snapshotFleetCutoverCensusCandidate({
|
|
1010
|
+
clusterId: value.clusterId,
|
|
1011
|
+
runtime: value.runtime,
|
|
1012
|
+
targetSet: value.targetSet,
|
|
1013
|
+
inventory: value.inventory,
|
|
1014
|
+
managerReads: value.managerReads,
|
|
1015
|
+
nodeObservations: value.nodeObservations,
|
|
1016
|
+
freshUntil: value.freshUntil,
|
|
1017
|
+
});
|
|
1018
|
+
const acceptedAt = networkCounter(value.acceptedAt);
|
|
1019
|
+
requireNetwork(acceptedAt <= candidate.freshUntil
|
|
1020
|
+
&& candidate.managerReads.every(itemArg => acceptedAt >= itemArg.acceptedAt)
|
|
1021
|
+
&& candidate.nodeObservations.every(itemArg => acceptedAt >= itemArg.acceptedAt));
|
|
1022
|
+
return {
|
|
1023
|
+
id: networkId(value.id),
|
|
1024
|
+
generation: networkCounter(value.generation),
|
|
1025
|
+
acceptedAt,
|
|
1026
|
+
digest: networkDigest(value.digest),
|
|
1027
|
+
...candidate,
|
|
1028
|
+
};
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
export const computeFleetCutoverCensusDigest = async (
|
|
1032
|
+
valueArg: unknown,
|
|
1033
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1034
|
+
fleetCutoverContract.censusDigestDomain,
|
|
1035
|
+
await snapshotFleetCutoverCensus(valueArg),
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
export const validateFleetCutoverCensus = async (valueArg: unknown): Promise<string[]> => {
|
|
1039
|
+
try {
|
|
1040
|
+
const value = await snapshotFleetCutoverCensus(valueArg);
|
|
1041
|
+
return value.digest === await computeFleetCutoverCensusDigest(value)
|
|
1042
|
+
? [] : ['fleet cutover census digest does not match'];
|
|
1043
|
+
} catch {
|
|
1044
|
+
return ['fleet cutover census must use its exact canonical schema'];
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
export const fleetCutoverEvidenceReferenceOf = (
|
|
1049
|
+
valueArg: { id: string; generation: number; digest: TSha256Digest },
|
|
1050
|
+
): IFleetCutoverAcceptedEvidenceReference => reference({
|
|
1051
|
+
id: valueArg.id,
|
|
1052
|
+
generation: valueArg.generation,
|
|
1053
|
+
digest: valueArg.digest,
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
export const snapshotFleetCutoverAcceptedEvidenceReference = (
|
|
1057
|
+
valueArg: unknown,
|
|
1058
|
+
): IFleetCutoverAcceptedEvidenceReference => safe(() => reference(captureNetwork(valueArg)));
|
|
1059
|
+
|
|
1060
|
+
const scopeEntry = (valueArg: unknown): IFleetCutoverScopeEntry => {
|
|
1061
|
+
const value = networkRecord(valueArg, [
|
|
1062
|
+
'dockerServiceId', 'serviceId', 'owner', 'serviceAuthority', 'classificationAuthority',
|
|
1063
|
+
]);
|
|
1064
|
+
const owner = literal([
|
|
1065
|
+
'application', 'coreflow', 'corestore', 'coretraffic', 'unknown',
|
|
1066
|
+
] as const, value.owner);
|
|
1067
|
+
const serviceId = value.serviceId === null ? null : networkId(value.serviceId);
|
|
1068
|
+
const serviceAuthority = value.serviceAuthority === null ? null : reference(value.serviceAuthority);
|
|
1069
|
+
const classificationAuthority = value.classificationAuthority === null
|
|
1070
|
+
? null : reference(value.classificationAuthority);
|
|
1071
|
+
requireNetwork(owner === 'unknown'
|
|
1072
|
+
? serviceId === null && serviceAuthority === null && classificationAuthority === null
|
|
1073
|
+
: serviceId !== null && serviceAuthority !== null && classificationAuthority !== null);
|
|
1074
|
+
return {
|
|
1075
|
+
dockerServiceId: networkId(value.dockerServiceId),
|
|
1076
|
+
serviceId,
|
|
1077
|
+
owner,
|
|
1078
|
+
serviceAuthority,
|
|
1079
|
+
classificationAuthority,
|
|
1080
|
+
};
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
export const snapshotFleetCutoverScope = (valueArg: unknown): IFleetCutoverScope => safe(() => {
|
|
1084
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
1085
|
+
'id', 'generation', 'previous', 'state', 'clusterId', 'census', 'runtime', 'targetSet', 'entries',
|
|
1086
|
+
'actorId', 'acceptedAt', 'digest',
|
|
1087
|
+
]);
|
|
1088
|
+
const state = literal(['incomplete', 'complete'] as const, value.state);
|
|
1089
|
+
const id = networkId(value.id);
|
|
1090
|
+
const generation = networkCounter(value.generation);
|
|
1091
|
+
const previous = value.previous === null ? null : reference(value.previous);
|
|
1092
|
+
const clusterId = networkId(value.clusterId);
|
|
1093
|
+
const runtime = runtimeReference(value.runtime);
|
|
1094
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
1095
|
+
const entries = networkSorted(networkList(
|
|
1096
|
+
value.entries,
|
|
1097
|
+
fleetCutoverContract.maximumServices,
|
|
1098
|
+
scopeEntry,
|
|
1099
|
+
), itemArg => itemArg.dockerServiceId);
|
|
1100
|
+
requireNetwork(runtime.id === clusterId
|
|
1101
|
+
&& targetSet.cloudlyClusterId === clusterId
|
|
1102
|
+
&& (generation === 1
|
|
1103
|
+
? previous === null && state === 'incomplete'
|
|
1104
|
+
: previous?.id === id && previous.generation === generation - 1)
|
|
1105
|
+
&& (state === 'incomplete' || entries.every(itemArg => itemArg.owner !== 'unknown')));
|
|
1106
|
+
return {
|
|
1107
|
+
id,
|
|
1108
|
+
generation,
|
|
1109
|
+
previous,
|
|
1110
|
+
state,
|
|
1111
|
+
clusterId,
|
|
1112
|
+
census: reference(value.census),
|
|
1113
|
+
runtime,
|
|
1114
|
+
targetSet,
|
|
1115
|
+
entries,
|
|
1116
|
+
actorId: networkId(value.actorId),
|
|
1117
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
1118
|
+
digest: networkDigest(value.digest),
|
|
1119
|
+
};
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
export const computeFleetCutoverScopeDigest = async (
|
|
1123
|
+
valueArg: unknown,
|
|
1124
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1125
|
+
fleetCutoverContract.scopeDigestDomain,
|
|
1126
|
+
snapshotFleetCutoverScope(valueArg),
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
export const validateFleetCutoverScope = async (valueArg: unknown): Promise<string[]> => {
|
|
1130
|
+
try {
|
|
1131
|
+
const value = snapshotFleetCutoverScope(valueArg);
|
|
1132
|
+
return value.digest === await computeFleetCutoverScopeDigest(value)
|
|
1133
|
+
? [] : ['fleet cutover scope digest does not match'];
|
|
1134
|
+
} catch {
|
|
1135
|
+
return ['fleet cutover scope must use its exact canonical schema'];
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
export const snapshotFleetCutoverScopeClassificationRequest = (
|
|
1140
|
+
valueArg: unknown,
|
|
1141
|
+
): IFleetCutoverScopeClassificationRequest => safe(() => {
|
|
1142
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
1143
|
+
'mutationId', 'clusterId', 'expectedScopeGeneration', 'census', 'classifications',
|
|
1144
|
+
]);
|
|
1145
|
+
const classifications = networkSorted(networkList(
|
|
1146
|
+
value.classifications,
|
|
1147
|
+
fleetCutoverContract.maximumClassifications,
|
|
1148
|
+
itemArg => {
|
|
1149
|
+
const item = networkRecord(itemArg, ['dockerServiceId', 'serviceId', 'owner']);
|
|
1150
|
+
return {
|
|
1151
|
+
dockerServiceId: networkId(item.dockerServiceId),
|
|
1152
|
+
serviceId: networkId(item.serviceId),
|
|
1153
|
+
owner: literal(['application', 'coreflow', 'corestore', 'coretraffic'] as const, item.owner),
|
|
1154
|
+
};
|
|
1155
|
+
},
|
|
1156
|
+
), itemArg => itemArg.dockerServiceId);
|
|
1157
|
+
return {
|
|
1158
|
+
mutationId: networkId(value.mutationId),
|
|
1159
|
+
clusterId: networkId(value.clusterId),
|
|
1160
|
+
expectedScopeGeneration: networkCounter(value.expectedScopeGeneration),
|
|
1161
|
+
census: reference(value.census),
|
|
1162
|
+
classifications,
|
|
1163
|
+
};
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
export const snapshotFleetCutoverTargetSetReference = (
|
|
1167
|
+
valueArg: unknown,
|
|
1168
|
+
): IFleetCutoverTargetSetReference => safe(() => targetSetReference(captureNetwork(valueArg)));
|
|
1169
|
+
|
|
1170
|
+
const volumeHandoff = (valueArg: unknown): IFleetCutoverVolumeHandoffIntent => {
|
|
1171
|
+
const value = networkRecord(valueArg, [
|
|
1172
|
+
'id', 'generation', 'clusterId', 'nodeId', 'serviceId', 'volumeId',
|
|
1173
|
+
'mountIdentityDigest', 'sourceLineageDigest', 'storageRequestDigest', 'runtime', 'scope',
|
|
1174
|
+
'targetSet', 'legacyStorageEvidence', 'sourceNodeObservation', 'acceptedAt', 'digest',
|
|
1175
|
+
]);
|
|
1176
|
+
const clusterId = networkId(value.clusterId);
|
|
1177
|
+
const runtime = runtimeReference(value.runtime);
|
|
1178
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
1179
|
+
requireNetwork(runtime.id === clusterId && runtime.phase === 'draining'
|
|
1180
|
+
&& targetSet.cloudlyClusterId === clusterId);
|
|
1181
|
+
return {
|
|
1182
|
+
id: networkId(value.id),
|
|
1183
|
+
generation: networkCounter(value.generation),
|
|
1184
|
+
clusterId,
|
|
1185
|
+
nodeId: networkNodeId(value.nodeId),
|
|
1186
|
+
serviceId: networkId(value.serviceId),
|
|
1187
|
+
volumeId: networkId(value.volumeId),
|
|
1188
|
+
mountIdentityDigest: networkDigest(value.mountIdentityDigest),
|
|
1189
|
+
sourceLineageDigest: networkDigest(value.sourceLineageDigest),
|
|
1190
|
+
storageRequestDigest: networkDigest(value.storageRequestDigest),
|
|
1191
|
+
runtime: runtime as IFleetCutoverVolumeHandoffIntent['runtime'],
|
|
1192
|
+
scope: reference(value.scope),
|
|
1193
|
+
targetSet,
|
|
1194
|
+
legacyStorageEvidence: reference(value.legacyStorageEvidence),
|
|
1195
|
+
sourceNodeObservation: reference(value.sourceNodeObservation),
|
|
1196
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
1197
|
+
digest: networkDigest(value.digest),
|
|
1198
|
+
};
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
export const snapshotFleetCutoverVolumeHandoffIntent = (
|
|
1202
|
+
valueArg: unknown,
|
|
1203
|
+
): IFleetCutoverVolumeHandoffIntent => safe(() => volumeHandoff(captureNetwork(valueArg)));
|
|
1204
|
+
|
|
1205
|
+
export const computeFleetCutoverVolumeHandoffDigest = async (
|
|
1206
|
+
valueArg: unknown,
|
|
1207
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1208
|
+
fleetCutoverContract.volumeHandoffDigestDomain,
|
|
1209
|
+
snapshotFleetCutoverVolumeHandoffIntent(valueArg),
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
const volumeFence = (valueArg: unknown): TFleetCutoverVolumeFence => {
|
|
1213
|
+
requireNetwork(valueArg !== null && typeof valueArg === 'object');
|
|
1214
|
+
const state = (valueArg as Record<string, unknown>).state;
|
|
1215
|
+
const consumedKeys = state === 'consumed'
|
|
1216
|
+
? ['consumedRuntime', 'adoptionReceipt', 'claim', 'consumedAt'] : [];
|
|
1217
|
+
const value = networkRecord(valueArg, [
|
|
1218
|
+
'id', 'generation', 'previous', 'clusterId', 'nodeId', 'serviceId', 'volumeId',
|
|
1219
|
+
'mountIdentityDigest', 'handoffIntent', 'heldRuntime', 'heldAt', 'state', ...consumedKeys,
|
|
1220
|
+
'digest',
|
|
1221
|
+
]);
|
|
1222
|
+
requireNetwork(state === 'held' || state === 'consumed');
|
|
1223
|
+
const id = networkId(value.id);
|
|
1224
|
+
const generation = networkCounter(value.generation);
|
|
1225
|
+
const previous = value.previous === null ? null : reference(value.previous);
|
|
1226
|
+
requireNetwork(generation === 1
|
|
1227
|
+
? previous === null
|
|
1228
|
+
: previous?.id === id && previous.generation === generation - 1);
|
|
1229
|
+
const heldRuntime = runtimeReference(value.heldRuntime);
|
|
1230
|
+
requireNetwork(heldRuntime.phase === 'draining');
|
|
1231
|
+
const base: IFleetCutoverVolumeFenceBase = {
|
|
1232
|
+
id,
|
|
1233
|
+
generation,
|
|
1234
|
+
previous,
|
|
1235
|
+
clusterId: networkId(value.clusterId),
|
|
1236
|
+
nodeId: networkNodeId(value.nodeId),
|
|
1237
|
+
serviceId: networkId(value.serviceId),
|
|
1238
|
+
volumeId: networkId(value.volumeId),
|
|
1239
|
+
mountIdentityDigest: networkDigest(value.mountIdentityDigest),
|
|
1240
|
+
handoffIntent: reference(value.handoffIntent),
|
|
1241
|
+
heldRuntime: heldRuntime as IFleetCutoverVolumeFenceBase['heldRuntime'],
|
|
1242
|
+
heldAt: networkCounter(value.heldAt),
|
|
1243
|
+
digest: networkDigest(value.digest),
|
|
1244
|
+
};
|
|
1245
|
+
if (state === 'held') return { ...base, state };
|
|
1246
|
+
const consumedRuntime = runtimeReference(value.consumedRuntime);
|
|
1247
|
+
requireNetwork(consumedRuntime.phase === 'pallet');
|
|
1248
|
+
return {
|
|
1249
|
+
...base,
|
|
1250
|
+
state,
|
|
1251
|
+
consumedRuntime: consumedRuntime as Extract<TFleetCutoverVolumeFence, { state: 'consumed' }>['consumedRuntime'],
|
|
1252
|
+
adoptionReceipt: reference(value.adoptionReceipt),
|
|
1253
|
+
claim: reference(value.claim),
|
|
1254
|
+
consumedAt: networkCounter(value.consumedAt),
|
|
1255
|
+
};
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
export const snapshotFleetCutoverVolumeFence = (
|
|
1259
|
+
valueArg: unknown,
|
|
1260
|
+
): TFleetCutoverVolumeFence => safe(() => volumeFence(captureNetwork(valueArg)));
|
|
1261
|
+
|
|
1262
|
+
export const computeFleetCutoverVolumeFenceDigest = async (
|
|
1263
|
+
valueArg: unknown,
|
|
1264
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1265
|
+
fleetCutoverContract.volumeFenceDigestDomain,
|
|
1266
|
+
snapshotFleetCutoverVolumeFence(valueArg),
|
|
1267
|
+
);
|
|
1268
|
+
|
|
1269
|
+
const corestoreAdoption = (valueArg: unknown): IFleetCutoverCorestoreAdoptionReceipt => {
|
|
1270
|
+
const value = networkRecord(valueArg, [
|
|
1271
|
+
'id', 'generation', 'clusterId', 'nodeId', 'serviceId', 'volumeId',
|
|
1272
|
+
'mountIdentityDigest', 'handoffIntent', 'palletFence', 'sourceLineageDigest', 'claim',
|
|
1273
|
+
'accessMode', 'acceptedAt', 'digest',
|
|
1274
|
+
]);
|
|
1275
|
+
requireNetwork(value.generation === 1 && value.accessMode === 'ReadWriteOnce');
|
|
1276
|
+
return {
|
|
1277
|
+
id: networkId(value.id),
|
|
1278
|
+
generation: 1,
|
|
1279
|
+
clusterId: networkId(value.clusterId),
|
|
1280
|
+
nodeId: networkNodeId(value.nodeId),
|
|
1281
|
+
serviceId: networkId(value.serviceId),
|
|
1282
|
+
volumeId: networkId(value.volumeId),
|
|
1283
|
+
mountIdentityDigest: networkDigest(value.mountIdentityDigest),
|
|
1284
|
+
handoffIntent: reference(value.handoffIntent),
|
|
1285
|
+
palletFence: reference(value.palletFence),
|
|
1286
|
+
sourceLineageDigest: networkDigest(value.sourceLineageDigest),
|
|
1287
|
+
claim: reference(value.claim),
|
|
1288
|
+
accessMode: 'ReadWriteOnce',
|
|
1289
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
1290
|
+
digest: networkDigest(value.digest),
|
|
1291
|
+
};
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
export const snapshotFleetCutoverCorestoreAdoptionReceipt = (
|
|
1295
|
+
valueArg: unknown,
|
|
1296
|
+
): IFleetCutoverCorestoreAdoptionReceipt => safe(() => corestoreAdoption(captureNetwork(valueArg)));
|
|
1297
|
+
|
|
1298
|
+
export const computeFleetCutoverCorestoreAdoptionDigest = async (
|
|
1299
|
+
valueArg: unknown,
|
|
1300
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1301
|
+
fleetCutoverContract.corestoreAdoptionDigestDomain,
|
|
1302
|
+
snapshotFleetCutoverCorestoreAdoptionReceipt(valueArg),
|
|
1303
|
+
);
|
|
1304
|
+
|
|
1305
|
+
/** Content join only. The Pallet owner must dereference both accepted records while holding its CAS. */
|
|
1306
|
+
export const bindFleetCutoverAdoptionToVolumeFence = (
|
|
1307
|
+
adoptionArg: unknown,
|
|
1308
|
+
fenceArg: unknown,
|
|
1309
|
+
): { adoption: IFleetCutoverCorestoreAdoptionReceipt; fence: TFleetCutoverVolumeFence } => safe(() => {
|
|
1310
|
+
const adoption = snapshotFleetCutoverCorestoreAdoptionReceipt(adoptionArg);
|
|
1311
|
+
const fence = snapshotFleetCutoverVolumeFence(fenceArg);
|
|
1312
|
+
requireNetwork(fence.state === 'held'
|
|
1313
|
+
&& adoption.clusterId === fence.clusterId
|
|
1314
|
+
&& adoption.nodeId === fence.nodeId
|
|
1315
|
+
&& adoption.serviceId === fence.serviceId
|
|
1316
|
+
&& adoption.volumeId === fence.volumeId
|
|
1317
|
+
&& adoption.mountIdentityDigest === fence.mountIdentityDigest
|
|
1318
|
+
&& adoption.handoffIntent.id === fence.handoffIntent.id
|
|
1319
|
+
&& adoption.handoffIntent.generation === fence.handoffIntent.generation
|
|
1320
|
+
&& adoption.handoffIntent.digest === fence.handoffIntent.digest
|
|
1321
|
+
&& adoption.palletFence.id === fence.id
|
|
1322
|
+
&& adoption.palletFence.generation === fence.generation
|
|
1323
|
+
&& adoption.palletFence.digest === fence.digest);
|
|
1324
|
+
return { adoption, fence };
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
export const validateFleetCutoverVolumeHandoffIntent = async (valueArg: unknown): Promise<string[]> => {
|
|
1328
|
+
try {
|
|
1329
|
+
const value = snapshotFleetCutoverVolumeHandoffIntent(valueArg);
|
|
1330
|
+
return value.digest === await computeFleetCutoverVolumeHandoffDigest(value)
|
|
1331
|
+
? [] : ['fleet cutover volume handoff digest does not match'];
|
|
1332
|
+
} catch { return ['fleet cutover volume handoff must use its exact canonical schema']; }
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
export const validateFleetCutoverVolumeFence = async (valueArg: unknown): Promise<string[]> => {
|
|
1336
|
+
try {
|
|
1337
|
+
const value = snapshotFleetCutoverVolumeFence(valueArg);
|
|
1338
|
+
return value.digest === await computeFleetCutoverVolumeFenceDigest(value)
|
|
1339
|
+
? [] : ['fleet cutover volume fence digest does not match'];
|
|
1340
|
+
} catch { return ['fleet cutover volume fence must use its exact canonical schema']; }
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
export const validateFleetCutoverCorestoreAdoptionReceipt = async (
|
|
1344
|
+
valueArg: unknown,
|
|
1345
|
+
): Promise<string[]> => {
|
|
1346
|
+
try {
|
|
1347
|
+
const value = snapshotFleetCutoverCorestoreAdoptionReceipt(valueArg);
|
|
1348
|
+
return value.digest === await computeFleetCutoverCorestoreAdoptionDigest(value)
|
|
1349
|
+
? [] : ['fleet cutover Corestore adoption digest does not match'];
|
|
1350
|
+
} catch { return ['fleet cutover Corestore adoption receipt must use its exact canonical schema']; }
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
const runtimeSession = (valueArg: unknown): IRuntimeSessionBinding => {
|
|
1354
|
+
const value = networkRecord(valueArg, [
|
|
1355
|
+
'reporterSessionId', 'nodeId', 'clusterId', 'credentialGeneration', 'sessionEpoch',
|
|
1356
|
+
'controllerInstanceId', 'controller', 'runtimeNamespace',
|
|
1357
|
+
]);
|
|
1358
|
+
const controller = networkRecord(value.controller, ['kind', 'id', 'epoch']);
|
|
1359
|
+
requireNetwork(controller.kind === 'cloudly' || controller.kind === 'onebox');
|
|
1360
|
+
return {
|
|
1361
|
+
reporterSessionId: networkId(value.reporterSessionId),
|
|
1362
|
+
nodeId: networkNodeId(value.nodeId),
|
|
1363
|
+
clusterId: networkId(value.clusterId),
|
|
1364
|
+
credentialGeneration: networkCounter(value.credentialGeneration),
|
|
1365
|
+
sessionEpoch: networkCounter(value.sessionEpoch),
|
|
1366
|
+
controllerInstanceId: networkId(value.controllerInstanceId),
|
|
1367
|
+
controller: {
|
|
1368
|
+
kind: controller.kind,
|
|
1369
|
+
id: networkId(controller.id),
|
|
1370
|
+
epoch: networkId(controller.epoch),
|
|
1371
|
+
},
|
|
1372
|
+
runtimeNamespace: networkId(value.runtimeNamespace),
|
|
1373
|
+
};
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
export const snapshotFleetCutoverCoreflowRetirementEvidence = (
|
|
1377
|
+
valueArg: unknown,
|
|
1378
|
+
): IFleetCutoverCoreflowRetirementEvidence => safe(() => {
|
|
1379
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
1380
|
+
'id', 'generation', 'clusterId', 'scope', 'targetSet', 'runtime', 'relay', 'spark',
|
|
1381
|
+
'legacy', 'acceptedAt', 'digest',
|
|
1382
|
+
]);
|
|
1383
|
+
const relay = networkRecord(value.relay, [
|
|
1384
|
+
'serviceId', 'nodeId', 'dockerServiceId', 'dockerContainerId', 'specVersion', 'specDigest',
|
|
1385
|
+
'imageDigest', 'managerObservation', 'nodeObservation', 'credential',
|
|
1386
|
+
'acceptedRegistration', 'version',
|
|
1387
|
+
]);
|
|
1388
|
+
const spark = networkRecord(value.spark, [
|
|
1389
|
+
'nodeId', 'credentialGeneration', 'reporterSessionId', 'observationSequence',
|
|
1390
|
+
'observation', 'bundleVersion', 'bundleSourceCommit',
|
|
1391
|
+
'bundleManifestSha256', 'unitName', 'unitDigest',
|
|
1392
|
+
]);
|
|
1393
|
+
const legacy = networkRecord(value.legacy, [
|
|
1394
|
+
'credential', 'invalidation', 'oldModeRefusal', 'configurationCas',
|
|
1395
|
+
'sourceOwnerGeneration', 'sourceSha256', 'sourceDisposition',
|
|
1396
|
+
]);
|
|
1397
|
+
const clusterId = networkId(value.clusterId);
|
|
1398
|
+
const runtime = runtimeReference(value.runtime);
|
|
1399
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
1400
|
+
requireNetwork(runtime.id === clusterId && runtime.phase === 'swarm'
|
|
1401
|
+
&& targetSet.cloudlyClusterId === clusterId);
|
|
1402
|
+
const relayNodeId = networkNodeId(relay.nodeId);
|
|
1403
|
+
const sparkNodeId = networkNodeId(spark.nodeId);
|
|
1404
|
+
requireNetwork(relayNodeId === sparkNodeId);
|
|
1405
|
+
const relayNodeObservation = reference(relay.nodeObservation);
|
|
1406
|
+
const sparkObservation = reference(spark.observation);
|
|
1407
|
+
requireNetwork(relayNodeObservation.id === sparkObservation.id
|
|
1408
|
+
&& relayNodeObservation.generation === sparkObservation.generation
|
|
1409
|
+
&& relayNodeObservation.digest === sparkObservation.digest);
|
|
1410
|
+
const relayVersion = releaseVersion(relay.version);
|
|
1411
|
+
requireNetwork(relayVersion.startsWith('32.'));
|
|
1412
|
+
return {
|
|
1413
|
+
id: networkId(value.id),
|
|
1414
|
+
generation: networkCounter(value.generation),
|
|
1415
|
+
clusterId,
|
|
1416
|
+
scope: reference(value.scope),
|
|
1417
|
+
targetSet,
|
|
1418
|
+
runtime,
|
|
1419
|
+
relay: {
|
|
1420
|
+
serviceId: networkId(relay.serviceId),
|
|
1421
|
+
nodeId: relayNodeId,
|
|
1422
|
+
dockerServiceId: networkId(relay.dockerServiceId),
|
|
1423
|
+
dockerContainerId: networkId(relay.dockerContainerId),
|
|
1424
|
+
specVersion: networkCounter(relay.specVersion),
|
|
1425
|
+
specDigest: networkDigest(relay.specDigest),
|
|
1426
|
+
imageDigest: networkDigest(relay.imageDigest),
|
|
1427
|
+
managerObservation: reference(relay.managerObservation),
|
|
1428
|
+
nodeObservation: relayNodeObservation,
|
|
1429
|
+
credential: reference(relay.credential),
|
|
1430
|
+
acceptedRegistration: reference(relay.acceptedRegistration),
|
|
1431
|
+
version: relayVersion,
|
|
1432
|
+
},
|
|
1433
|
+
spark: {
|
|
1434
|
+
nodeId: sparkNodeId,
|
|
1435
|
+
credentialGeneration: networkCounter(spark.credentialGeneration),
|
|
1436
|
+
reporterSessionId: networkId(spark.reporterSessionId),
|
|
1437
|
+
observationSequence: networkCounter(spark.observationSequence),
|
|
1438
|
+
observation: sparkObservation,
|
|
1439
|
+
bundleVersion: releaseVersion(spark.bundleVersion),
|
|
1440
|
+
bundleSourceCommit: (() => {
|
|
1441
|
+
requireNetwork(typeof spark.bundleSourceCommit === 'string'
|
|
1442
|
+
&& /^[a-f0-9]{40}$/.test(spark.bundleSourceCommit));
|
|
1443
|
+
return spark.bundleSourceCommit;
|
|
1444
|
+
})(),
|
|
1445
|
+
bundleManifestSha256: sha256Hex(spark.bundleManifestSha256),
|
|
1446
|
+
unitName: networkId(spark.unitName),
|
|
1447
|
+
unitDigest: networkDigest(spark.unitDigest),
|
|
1448
|
+
},
|
|
1449
|
+
legacy: {
|
|
1450
|
+
credential: reference(legacy.credential),
|
|
1451
|
+
invalidation: reference(legacy.invalidation),
|
|
1452
|
+
oldModeRefusal: reference(legacy.oldModeRefusal),
|
|
1453
|
+
configurationCas: reference(legacy.configurationCas),
|
|
1454
|
+
sourceOwnerGeneration: networkCounter(legacy.sourceOwnerGeneration),
|
|
1455
|
+
sourceSha256: sha256Hex(legacy.sourceSha256),
|
|
1456
|
+
sourceDisposition: literal(['retained-inert', 'removed'] as const, legacy.sourceDisposition),
|
|
1457
|
+
},
|
|
1458
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
1459
|
+
digest: networkDigest(value.digest),
|
|
1460
|
+
};
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
export const computeFleetCutoverCoreflowRetirementDigest = async (
|
|
1464
|
+
valueArg: unknown,
|
|
1465
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1466
|
+
fleetCutoverContract.coreflowRetirementDigestDomain,
|
|
1467
|
+
snapshotFleetCutoverCoreflowRetirementEvidence(valueArg),
|
|
1468
|
+
);
|
|
1469
|
+
|
|
1470
|
+
export const validateFleetCutoverCoreflowRetirementEvidence = async (
|
|
1471
|
+
valueArg: unknown,
|
|
1472
|
+
): Promise<string[]> => {
|
|
1473
|
+
try {
|
|
1474
|
+
const value = snapshotFleetCutoverCoreflowRetirementEvidence(valueArg);
|
|
1475
|
+
return value.digest === await computeFleetCutoverCoreflowRetirementDigest(value)
|
|
1476
|
+
? [] : ['fleet cutover Coreflow retirement digest does not match'];
|
|
1477
|
+
} catch { return ['fleet cutover Coreflow retirement evidence must use its exact canonical schema']; }
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
export const snapshotFleetCutoverIngressPreparationRequest = (
|
|
1481
|
+
valueArg: unknown,
|
|
1482
|
+
): IFleetCutoverIngressPreparationRequest => safe(() => {
|
|
1483
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
1484
|
+
'mutationId', 'clusterId', 'ingressServiceId', 'currentRuntime', 'scope', 'targetSet',
|
|
1485
|
+
'expectedSpecRevision', 'expectedDesignation',
|
|
1486
|
+
]);
|
|
1487
|
+
const clusterId = networkId(value.clusterId);
|
|
1488
|
+
const currentRuntime = runtimeReference(value.currentRuntime);
|
|
1489
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
1490
|
+
requireNetwork(currentRuntime.id === clusterId && currentRuntime.phase === 'pallet'
|
|
1491
|
+
&& targetSet.cloudlyClusterId === clusterId);
|
|
1492
|
+
return {
|
|
1493
|
+
mutationId: networkId(value.mutationId),
|
|
1494
|
+
clusterId,
|
|
1495
|
+
ingressServiceId: networkId(value.ingressServiceId),
|
|
1496
|
+
currentRuntime: currentRuntime as IFleetCutoverIngressPreparationRequest['currentRuntime'],
|
|
1497
|
+
scope: reference(value.scope),
|
|
1498
|
+
targetSet,
|
|
1499
|
+
expectedSpecRevision: networkCounter(value.expectedSpecRevision),
|
|
1500
|
+
expectedDesignation: value.expectedDesignation === null
|
|
1501
|
+
? null : reference(value.expectedDesignation),
|
|
1502
|
+
};
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
export const snapshotFleetCutoverIngressDesignation = (
|
|
1506
|
+
valueArg: unknown,
|
|
1507
|
+
): IFleetCutoverIngressDesignation => safe(() => {
|
|
1508
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
1509
|
+
'id', 'generation', 'previous', 'clusterId', 'runtime', 'scope', 'targetSet',
|
|
1510
|
+
'ingressServiceId', 'preparedSpecRevision', 'credential', 'actorId', 'acceptedAt', 'digest',
|
|
1511
|
+
]);
|
|
1512
|
+
const id = networkId(value.id);
|
|
1513
|
+
const generation = networkCounter(value.generation);
|
|
1514
|
+
const previous = value.previous === null ? null : reference(value.previous);
|
|
1515
|
+
const clusterId = networkId(value.clusterId);
|
|
1516
|
+
const runtime = runtimeReference(value.runtime);
|
|
1517
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
1518
|
+
requireNetwork(runtime.id === clusterId && runtime.phase === 'pallet'
|
|
1519
|
+
&& targetSet.cloudlyClusterId === clusterId
|
|
1520
|
+
&& (generation === 1
|
|
1521
|
+
? previous === null
|
|
1522
|
+
: previous?.id === id && previous.generation === generation - 1));
|
|
1523
|
+
return {
|
|
1524
|
+
id,
|
|
1525
|
+
generation,
|
|
1526
|
+
previous,
|
|
1527
|
+
clusterId,
|
|
1528
|
+
runtime: runtime as IFleetCutoverIngressDesignation['runtime'],
|
|
1529
|
+
scope: reference(value.scope),
|
|
1530
|
+
targetSet,
|
|
1531
|
+
ingressServiceId: networkId(value.ingressServiceId),
|
|
1532
|
+
preparedSpecRevision: networkCounter(value.preparedSpecRevision),
|
|
1533
|
+
credential: ingressCredentialReference(value.credential),
|
|
1534
|
+
actorId: networkId(value.actorId),
|
|
1535
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
1536
|
+
digest: networkDigest(value.digest),
|
|
1537
|
+
};
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
export const computeFleetCutoverIngressDesignationDigest = async (
|
|
1541
|
+
valueArg: unknown,
|
|
1542
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1543
|
+
fleetCutoverContract.ingressDesignationDigestDomain,
|
|
1544
|
+
snapshotFleetCutoverIngressDesignation(valueArg),
|
|
1545
|
+
);
|
|
1546
|
+
|
|
1547
|
+
export const validateFleetCutoverIngressDesignation = async (
|
|
1548
|
+
valueArg: unknown,
|
|
1549
|
+
): Promise<string[]> => {
|
|
1550
|
+
try {
|
|
1551
|
+
const value = snapshotFleetCutoverIngressDesignation(valueArg);
|
|
1552
|
+
return value.digest === await computeFleetCutoverIngressDesignationDigest(value)
|
|
1553
|
+
? [] : ['fleet cutover ingress designation digest does not match'];
|
|
1554
|
+
} catch { return ['fleet cutover ingress designation must use its exact canonical schema']; }
|
|
1555
|
+
};
|
|
1556
|
+
|
|
1557
|
+
export const snapshotFleetCutoverIngressAuthority = (
|
|
1558
|
+
valueArg: unknown,
|
|
1559
|
+
): IFleetCutoverIngressAuthority => safe(() => {
|
|
1560
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
1561
|
+
'id', 'generation', 'clusterId', 'runtime', 'scope', 'targetSet', 'ingressServiceId',
|
|
1562
|
+
'assignment', 'session', 'ingressRegistration', 'registrationRouteRevision',
|
|
1563
|
+
'legacyIngressAbsence', 'sourcePublication', 'targetPublication', 'portTransfer',
|
|
1564
|
+
'routeTableRevision', 'routeTableReadback', 'acceptedAt', 'digest',
|
|
1565
|
+
]);
|
|
1566
|
+
const clusterId = networkId(value.clusterId);
|
|
1567
|
+
const runtime = runtimeReference(value.runtime);
|
|
1568
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
1569
|
+
const session = runtimeSession(value.session);
|
|
1570
|
+
requireNetwork(runtime.id === clusterId && runtime.phase === 'pallet'
|
|
1571
|
+
&& targetSet.cloudlyClusterId === clusterId && session.clusterId === clusterId);
|
|
1572
|
+
return {
|
|
1573
|
+
id: networkId(value.id),
|
|
1574
|
+
generation: networkCounter(value.generation),
|
|
1575
|
+
clusterId,
|
|
1576
|
+
runtime: runtime as IFleetCutoverIngressAuthority['runtime'],
|
|
1577
|
+
scope: reference(value.scope),
|
|
1578
|
+
targetSet,
|
|
1579
|
+
ingressServiceId: networkId(value.ingressServiceId),
|
|
1580
|
+
assignment: assignmentReference(value.assignment),
|
|
1581
|
+
session,
|
|
1582
|
+
ingressRegistration: reference(value.ingressRegistration),
|
|
1583
|
+
registrationRouteRevision: value.registrationRouteRevision === null
|
|
1584
|
+
? null : nonNegative(value.registrationRouteRevision),
|
|
1585
|
+
legacyIngressAbsence: reference(value.legacyIngressAbsence),
|
|
1586
|
+
sourcePublication: reference(value.sourcePublication),
|
|
1587
|
+
targetPublication: reference(value.targetPublication),
|
|
1588
|
+
portTransfer: reference(value.portTransfer),
|
|
1589
|
+
routeTableRevision: nonNegative(value.routeTableRevision),
|
|
1590
|
+
routeTableReadback: reference(value.routeTableReadback),
|
|
1591
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
1592
|
+
digest: networkDigest(value.digest),
|
|
1593
|
+
};
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
export const computeFleetCutoverIngressAuthorityDigest = async (
|
|
1597
|
+
valueArg: unknown,
|
|
1598
|
+
): Promise<TSha256Digest> => canonicalDigest(
|
|
1599
|
+
fleetCutoverContract.ingressAuthorityDigestDomain,
|
|
1600
|
+
snapshotFleetCutoverIngressAuthority(valueArg),
|
|
1601
|
+
);
|
|
1602
|
+
|
|
1603
|
+
export const validateFleetCutoverIngressAuthority = async (valueArg: unknown): Promise<string[]> => {
|
|
1604
|
+
try {
|
|
1605
|
+
const value = snapshotFleetCutoverIngressAuthority(valueArg);
|
|
1606
|
+
return value.digest === await computeFleetCutoverIngressAuthorityDigest(value)
|
|
1607
|
+
? [] : ['fleet cutover ingress authority digest does not match'];
|
|
1608
|
+
} catch { return ['fleet cutover ingress authority must use its exact canonical schema']; }
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
export const isLegalFleetCutoverPhaseTransition = (
|
|
1612
|
+
fromArg: TClusterRuntimePhase,
|
|
1613
|
+
toArg: TClusterRuntimePhase,
|
|
1614
|
+
): boolean => fleetCutoverContract.legalPhaseTransitions[fromArg] === toArg;
|
|
1615
|
+
|
|
1616
|
+
export const snapshotFleetCutoverPhaseAdvance = (
|
|
1617
|
+
valueArg: unknown,
|
|
1618
|
+
): TFleetCutoverPhaseAdvance => safe(() => {
|
|
1619
|
+
requireNetwork(valueArg !== null && typeof valueArg === 'object');
|
|
1620
|
+
const candidate = valueArg as Record<string, unknown>;
|
|
1621
|
+
const from = literal(['swarm', 'draining', 'switching'] as const, candidate.from);
|
|
1622
|
+
const expectedTo = fleetCutoverContract.legalPhaseTransitions[from];
|
|
1623
|
+
requireNetwork(candidate.to === expectedTo);
|
|
1624
|
+
const extraKeys = from === 'swarm'
|
|
1625
|
+
? ['coreflowRetirement']
|
|
1626
|
+
: from === 'draining'
|
|
1627
|
+
? ['swarmQuiescence', 'heldRuntimeSpecs', 'heldVolumeFences']
|
|
1628
|
+
: ['heldRuntimeSpecs', 'heldVolumeFences', 'palletPreparation'];
|
|
1629
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
1630
|
+
'mutationId', 'clusterId', 'currentRuntime', 'scope', 'targetSet', 'from', 'to', ...extraKeys,
|
|
1631
|
+
]);
|
|
1632
|
+
const currentRuntime = runtimeReference(value.currentRuntime);
|
|
1633
|
+
const clusterId = networkId(value.clusterId);
|
|
1634
|
+
const targetSet = targetSetReference(value.targetSet);
|
|
1635
|
+
requireNetwork(currentRuntime.id === clusterId && currentRuntime.phase === from
|
|
1636
|
+
&& targetSet.cloudlyClusterId === clusterId);
|
|
1637
|
+
const base: IFleetCutoverPhaseAdvanceBase = {
|
|
1638
|
+
mutationId: networkId(value.mutationId),
|
|
1639
|
+
clusterId,
|
|
1640
|
+
currentRuntime,
|
|
1641
|
+
scope: reference(value.scope),
|
|
1642
|
+
targetSet,
|
|
1643
|
+
};
|
|
1644
|
+
if (from === 'swarm') return {
|
|
1645
|
+
...base,
|
|
1646
|
+
from,
|
|
1647
|
+
to: 'draining',
|
|
1648
|
+
coreflowRetirement: reference(value.coreflowRetirement),
|
|
1649
|
+
};
|
|
1650
|
+
if (from === 'draining') return {
|
|
1651
|
+
...base,
|
|
1652
|
+
from,
|
|
1653
|
+
to: 'switching',
|
|
1654
|
+
swarmQuiescence: reference(value.swarmQuiescence),
|
|
1655
|
+
heldRuntimeSpecs: reference(value.heldRuntimeSpecs),
|
|
1656
|
+
heldVolumeFences: reference(value.heldVolumeFences),
|
|
1657
|
+
};
|
|
1658
|
+
return {
|
|
1659
|
+
...base,
|
|
1660
|
+
from,
|
|
1661
|
+
to: 'pallet',
|
|
1662
|
+
heldRuntimeSpecs: reference(value.heldRuntimeSpecs),
|
|
1663
|
+
heldVolumeFences: reference(value.heldVolumeFences),
|
|
1664
|
+
palletPreparation: reference(value.palletPreparation),
|
|
1665
|
+
};
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
export const snapshotFleetCutoverRuntimeSpecRelease = (
|
|
1669
|
+
valueArg: unknown,
|
|
1670
|
+
): TFleetCutoverRuntimeSpecRelease => safe(() => {
|
|
1671
|
+
requireNetwork(valueArg !== null && typeof valueArg === 'object');
|
|
1672
|
+
const kind = literal(['ingress', 'application'] as const, (valueArg as Record<string, unknown>).kind);
|
|
1673
|
+
const evidenceKey = kind === 'ingress' ? 'emptyPublishedPortsPolicy' : 'ingressAuthority';
|
|
1674
|
+
const value = networkRecord(captureNetwork(valueArg), [
|
|
1675
|
+
'kind', 'mutationId', 'clusterId', 'serviceId', 'expectedSpecRevision', 'runtime', 'scope',
|
|
1676
|
+
evidenceKey,
|
|
1677
|
+
]);
|
|
1678
|
+
const runtime = runtimeReference(value.runtime);
|
|
1679
|
+
const clusterId = networkId(value.clusterId);
|
|
1680
|
+
requireNetwork(runtime.id === clusterId && runtime.phase === 'pallet');
|
|
1681
|
+
const base = {
|
|
1682
|
+
kind,
|
|
1683
|
+
mutationId: networkId(value.mutationId),
|
|
1684
|
+
clusterId,
|
|
1685
|
+
serviceId: networkId(value.serviceId),
|
|
1686
|
+
expectedSpecRevision: networkCounter(value.expectedSpecRevision),
|
|
1687
|
+
runtime: runtime as TFleetCutoverRuntimeSpecRelease['runtime'],
|
|
1688
|
+
scope: reference(value.scope),
|
|
1689
|
+
};
|
|
1690
|
+
return kind === 'ingress'
|
|
1691
|
+
? { ...base, kind, emptyPublishedPortsPolicy: reference(value.emptyPublishedPortsPolicy) }
|
|
1692
|
+
: { ...base, kind, ingressAuthority: reference(value.ingressAuthority) };
|
|
1693
|
+
});
|