@serve.zone/interfaces 32.21.0 → 32.23.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 +91 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/clusteringress.d.ts +58 -0
- package/dist_ts/data/clusteringress.js +96 -2
- package/dist_ts/data/clusteringressdesignation.d.ts +64 -0
- package/dist_ts/data/clusteringressdesignation.js +84 -0
- package/dist_ts/data/corestorelegacyadoption.d.ts +178 -0
- package/dist_ts/data/corestorelegacyadoption.js +289 -0
- package/dist_ts/data/index.d.ts +6 -0
- package/dist_ts/data/index.js +6 -1
- package/dist_ts/data/networkprojection.d.ts +18 -0
- package/dist_ts/data/networkprojection.js +31 -2
- package/dist_ts/data/networkprotection.d.ts +13 -0
- package/dist_ts/data/networkprotection.js +8 -2
- package/dist_ts/data/networkworkloadingress.d.ts +61 -0
- package/dist_ts/data/networkworkloadingress.js +54 -0
- package/dist_ts/data/runtimelocalstorageclaim.d.ts +124 -0
- package/dist_ts/data/runtimelocalstorageclaim.js +167 -0
- package/dist_ts/data/service.d.ts +15 -1
- package/dist_ts/data/service.js +1 -1
- package/dist_ts/data/serviceruntime.d.ts +15 -3
- package/dist_ts/data/serviceruntime.js +7 -2
- package/dist_ts/data/servicevolumeinitialization.d.ts +91 -0
- package/dist_ts/data/servicevolumeinitialization.js +93 -0
- package/dist_ts/platform/types.d.ts +45 -0
- package/dist_ts/platform/types.js +27 -2
- package/dist_ts/private/storagecontract.d.ts +1 -0
- package/dist_ts/private/storagecontract.js +2 -1
- package/dist_ts/requests/clusteringress.d.ts +52 -0
- package/dist_ts/requests/clusteringress.js +1 -1
- package/dist_ts/requests/corestore.d.ts +60 -0
- package/dist_ts/requests/corestore.js +1 -1
- package/dist_ts/requests/platform.d.ts +6 -1
- package/dist_ts/requests/runtimesession.d.ts +12 -0
- package/dist_ts/requests/service.d.ts +23 -0
- package/package.json +1 -1
- package/readme.md +262 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/clusteringress.ts +136 -1
- package/ts/data/clusteringressdesignation.ts +141 -0
- package/ts/data/corestorelegacyadoption.ts +471 -0
- package/ts/data/index.ts +7 -0
- package/ts/data/networkprojection.ts +49 -1
- package/ts/data/networkprotection.ts +20 -1
- package/ts/data/networkworkloadingress.ts +107 -0
- package/ts/data/runtimelocalstorageclaim.ts +275 -0
- package/ts/data/service.ts +16 -0
- package/ts/data/serviceruntime.ts +19 -2
- package/ts/data/servicevolumeinitialization.ts +159 -0
- package/ts/platform/types.ts +75 -0
- package/ts/private/storagecontract.ts +1 -0
- package/ts/requests/clusteringress.ts +65 -0
- package/ts/requests/corestore.ts +79 -0
- package/ts/requests/platform.ts +6 -0
- package/ts/requests/runtimesession.ts +16 -0
- package/ts/requests/service.ts +28 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import type { IObjectStorageRetentionIntent } from '../platform/objectstorageretention.js';
|
|
2
|
+
import {
|
|
3
|
+
objectStorageRetentionIntentsEqual,
|
|
4
|
+
validateObjectStorageRetentionIntent,
|
|
5
|
+
} from '../platform/objectstorageretention.js';
|
|
6
|
+
import { validatePlatformObjectStorageBucketName } from '../platform/objectstorage.js';
|
|
7
|
+
import type { TPlatformBindingStatusReason } from '../platform/types.js';
|
|
8
|
+
import { platformBindingStatusReasons } from '../platform/types.js';
|
|
9
|
+
import {
|
|
10
|
+
canonicalizeStrictJson,
|
|
11
|
+
compareWireTokens,
|
|
12
|
+
createCanonicalJsonSha256Hex,
|
|
13
|
+
} from '../private/canonicaljson.js';
|
|
14
|
+
import { hasExactKeys, isPlainRecord } from '../private/dataproperties.js';
|
|
15
|
+
import { isClusterNodeId } from '../private/nodeidentifier.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Moving a service's legacy Corestore resources into credential bindings.
|
|
19
|
+
*
|
|
20
|
+
* A Corestore root converted from a release before 32 still holds the databases and buckets
|
|
21
|
+
* `/resources/provision` created, one per service and capability, in its manifest. Corestore
|
|
22
|
+
* refuses a credential binding beside such a resource until an operator authorizes that exact
|
|
23
|
+
* binding to adopt it, offline, with a plan file. The flow across the three owners:
|
|
24
|
+
*
|
|
25
|
+
* 1. Corestore answers `GET /control/legacy-resources` with its `ICorestoreLegacyResourceInventory`;
|
|
26
|
+
* the cluster relay reads it for Cloudly (`getClusterCorestoreLegacyResources`).
|
|
27
|
+
* 2. `adoptCorestoreLegacyResources` checks the admin's selection against that inventory at an
|
|
28
|
+
* exact digest, creates each adopting binding with the legacy bucket and retention intent, and
|
|
29
|
+
* answers the plan (`ICorestoreLegacyResourceAdoptionPlanEntry[]`).
|
|
30
|
+
* 3. The operator stops Corestore and runs its `legacy-resource-adoption` migration with that plan.
|
|
31
|
+
* Until then Corestore refuses the binding, which reports `legacy-adoption-pending`.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export type TCorestoreLegacyCapability = 'database' | 'objectstorage';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Why Corestore's offline adoption would refuse an unadopted resource:
|
|
38
|
+
*
|
|
39
|
+
* - `not-adoptable`: its identity is not what a credential binding can adopt (an
|
|
40
|
+
* allocation-managed database, another provider, a resource name that differs from its database
|
|
41
|
+
* or bucket name, or a bucket name that is not a canonical S3 name);
|
|
42
|
+
* - `ordinary-credentials-rotated`: its credentials were rotated, so the delivered ones are no
|
|
43
|
+
* longer the generation a binding adopts;
|
|
44
|
+
* - `owned-by-binding`: a credential binding or a named storage binding already owns the service
|
|
45
|
+
* capability, the resource or the bucket.
|
|
46
|
+
*/
|
|
47
|
+
export type TCorestoreLegacyResourceBlocker =
|
|
48
|
+
| 'not-adoptable'
|
|
49
|
+
| 'ordinary-credentials-rotated'
|
|
50
|
+
| 'owned-by-binding';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `unadopted` names what would stop the offline adoption, or `null` when nothing would;
|
|
54
|
+
* `authorized` names the binding the operator authorized, which is the only binding Corestore will
|
|
55
|
+
* let take the resource. A consumed adoption leaves no legacy resource behind, so it never appears.
|
|
56
|
+
*/
|
|
57
|
+
export type TCorestoreLegacyResourceAdoption =
|
|
58
|
+
| { state: 'unadopted'; blocker: TCorestoreLegacyResourceBlocker | null }
|
|
59
|
+
| { state: 'authorized'; bindingId: string };
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* One legacy resource, by the identity Corestore's manifest states. A database is named by its
|
|
63
|
+
* database name, a bucket by its bucket name and its retention intent, `null` when it has none.
|
|
64
|
+
*/
|
|
65
|
+
export type TCorestoreLegacyResource =
|
|
66
|
+
| { capability: 'database'; databaseName: string; adoption: TCorestoreLegacyResourceAdoption }
|
|
67
|
+
| {
|
|
68
|
+
capability: 'objectstorage';
|
|
69
|
+
bucketName: string;
|
|
70
|
+
retention: IObjectStorageRetentionIntent | null;
|
|
71
|
+
adoption: TCorestoreLegacyResourceAdoption;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** One service's legacy resources, at most one per capability, `database` first. */
|
|
75
|
+
export interface ICorestoreLegacyServiceResources {
|
|
76
|
+
serviceId: string;
|
|
77
|
+
resources: TCorestoreLegacyResource[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** What Corestore answers on `GET /control/legacy-resources`: every service, sorted by id. */
|
|
81
|
+
export interface ICorestoreLegacyResourceInventory {
|
|
82
|
+
services: ICorestoreLegacyServiceResources[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* One node's legacy resources as the cluster relay read them. An unreachable node names why and
|
|
87
|
+
* lists nothing, because a silent empty list would read as a node with nothing to adopt.
|
|
88
|
+
*/
|
|
89
|
+
export type TClusterCorestoreLegacyResourceInventory =
|
|
90
|
+
| { nodeId: string; checkedAt: number; reachable: true; services: ICorestoreLegacyServiceResources[] }
|
|
91
|
+
| {
|
|
92
|
+
nodeId: string;
|
|
93
|
+
checkedAt: number;
|
|
94
|
+
reachable: false;
|
|
95
|
+
errorCode: 'CORESTORE_ENDPOINT_UNKNOWN' | 'CORESTORE_UNREACHABLE';
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** One resource an administrator selects for adoption. */
|
|
99
|
+
export interface ICorestoreLegacyResourceSelection {
|
|
100
|
+
serviceId: string;
|
|
101
|
+
capability: TCorestoreLegacyCapability;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* One entry of Corestore's offline adoption plan, exactly the object its
|
|
106
|
+
* `legacy-resource-adoption` migration reads: the service, the capability and the binding that may
|
|
107
|
+
* adopt its legacy resource. The legacy identity is Corestore's own and is not restated here.
|
|
108
|
+
*/
|
|
109
|
+
export interface ICorestoreLegacyResourceAdoptionPlanEntry {
|
|
110
|
+
serviceId: string;
|
|
111
|
+
capability: TCorestoreLegacyCapability;
|
|
112
|
+
bindingId: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Why `adoptCorestoreLegacyResources` wrote nothing:
|
|
117
|
+
*
|
|
118
|
+
* - `legacy-inventory-unreachable`: the relay could not read the node's Corestore (retryable);
|
|
119
|
+
* - `legacy-inventory-changed`: the inventory no longer has `expectedInventorySha256`;
|
|
120
|
+
* - `legacy-resource-absent`: a selected service has no legacy resource of that capability;
|
|
121
|
+
* - `legacy-resource-blocked`: a selected resource is unadopted with a blocker;
|
|
122
|
+
* - `legacy-resource-authorized-elsewhere`: a selected resource is authorized for a binding Cloudly
|
|
123
|
+
* does not hold as that resource's adoption;
|
|
124
|
+
* - `service-absent`: Cloudly has no such service;
|
|
125
|
+
* - `binding-exists`: the service already has a binding of that capability that adopts nothing,
|
|
126
|
+
* or adopts another resource.
|
|
127
|
+
*/
|
|
128
|
+
export type TCorestoreLegacyAdoptionRefusal =
|
|
129
|
+
| 'legacy-inventory-unreachable'
|
|
130
|
+
| 'legacy-inventory-changed'
|
|
131
|
+
| 'legacy-resource-absent'
|
|
132
|
+
| 'legacy-resource-blocked'
|
|
133
|
+
| 'legacy-resource-authorized-elsewhere'
|
|
134
|
+
| 'service-absent'
|
|
135
|
+
| 'binding-exists';
|
|
136
|
+
|
|
137
|
+
/** The named refusal of `adoptCorestoreLegacyResources`, sent as `TypedResponseError.errorData`. */
|
|
138
|
+
export interface ICorestoreLegacyAdoptionErrorData {
|
|
139
|
+
code: TCorestoreLegacyAdoptionRefusal;
|
|
140
|
+
/** True for `legacy-inventory-unreachable` only; every other refusal needs a new read first. */
|
|
141
|
+
retryable: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const corestoreLegacyAdoptionContract = Object.freeze({
|
|
145
|
+
/** Corestore's control path for its legacy inventory, bearer-authenticated like every other. */
|
|
146
|
+
inventoryPath: '/control/legacy-resources',
|
|
147
|
+
/** Corestore's own plan bound, which also bounds a selection and an inventory. */
|
|
148
|
+
maximumPlanEntries: 4096,
|
|
149
|
+
maximumServices: 4096,
|
|
150
|
+
maximumNameLength: 255,
|
|
151
|
+
digestDomain: 'serve.zone/corestore-legacy-resources',
|
|
152
|
+
} as const);
|
|
153
|
+
|
|
154
|
+
const identifierPattern = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/;
|
|
155
|
+
const printableNamePattern = /^[\x21-\x7e]+$/;
|
|
156
|
+
const bareSha256Pattern = /^[a-f0-9]{64}$/;
|
|
157
|
+
const blockers: readonly TCorestoreLegacyResourceBlocker[] = [
|
|
158
|
+
'not-adoptable', 'ordinary-credentials-rotated', 'owned-by-binding',
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const isIdentifier = (valueArg: unknown): valueArg is string => (
|
|
162
|
+
typeof valueArg === 'string' && identifierPattern.test(valueArg)
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const isLegacyName = (valueArg: unknown): valueArg is string => (
|
|
166
|
+
typeof valueArg === 'string'
|
|
167
|
+
&& valueArg.length <= corestoreLegacyAdoptionContract.maximumNameLength
|
|
168
|
+
&& printableNamePattern.test(valueArg)
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const isCapability = (valueArg: unknown): valueArg is TCorestoreLegacyCapability => (
|
|
172
|
+
valueArg === 'database' || valueArg === 'objectstorage'
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const validateAdoption = (valueArg: unknown, pathArg: string): string[] => {
|
|
176
|
+
if (!isPlainRecord(valueArg)) return [`${pathArg} must use its exact schema`];
|
|
177
|
+
if (valueArg.state === 'unadopted') {
|
|
178
|
+
if (!hasExactKeys(valueArg, ['state', 'blocker'])) return [`${pathArg} must use its exact schema`];
|
|
179
|
+
return valueArg.blocker === null || (blockers as readonly unknown[]).includes(valueArg.blocker)
|
|
180
|
+
? []
|
|
181
|
+
: [`${pathArg} blocker must be null or a named blocker`];
|
|
182
|
+
}
|
|
183
|
+
if (valueArg.state === 'authorized') {
|
|
184
|
+
if (!hasExactKeys(valueArg, ['state', 'bindingId'])) return [`${pathArg} must use its exact schema`];
|
|
185
|
+
return isIdentifier(valueArg.bindingId) ? [] : [`${pathArg} bindingId must be a canonical identifier`];
|
|
186
|
+
}
|
|
187
|
+
return [`${pathArg} state must be unadopted or authorized`];
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** An adoptable resource is one the offline adoption could authorize or already did. */
|
|
191
|
+
const isAdoptable = (adoptionArg: unknown): boolean => (
|
|
192
|
+
isPlainRecord(adoptionArg) && (adoptionArg.state === 'authorized' || adoptionArg.blocker === null)
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const validateResource = (valueArg: unknown, pathArg: string): string[] => {
|
|
196
|
+
if (!isPlainRecord(valueArg)) return [`${pathArg} must use its exact schema`];
|
|
197
|
+
if (valueArg.capability === 'database') {
|
|
198
|
+
if (!hasExactKeys(valueArg, ['capability', 'databaseName', 'adoption'])) {
|
|
199
|
+
return [`${pathArg} must use its exact schema`];
|
|
200
|
+
}
|
|
201
|
+
const errors = validateAdoption(valueArg.adoption, `${pathArg} adoption`);
|
|
202
|
+
if (!isLegacyName(valueArg.databaseName)) errors.push(`${pathArg} databaseName must be a bounded printable name`);
|
|
203
|
+
return errors;
|
|
204
|
+
}
|
|
205
|
+
if (valueArg.capability === 'objectstorage') {
|
|
206
|
+
if (!hasExactKeys(valueArg, ['capability', 'bucketName', 'retention', 'adoption'])) {
|
|
207
|
+
return [`${pathArg} must use its exact schema`];
|
|
208
|
+
}
|
|
209
|
+
const errors = validateAdoption(valueArg.adoption, `${pathArg} adoption`);
|
|
210
|
+
if (!isLegacyName(valueArg.bucketName)) {
|
|
211
|
+
errors.push(`${pathArg} bucketName must be a bounded printable name`);
|
|
212
|
+
} else if (errors.length === 0 && isAdoptable(valueArg.adoption)) {
|
|
213
|
+
// A binding request carries only a canonical bucket, so an adoptable bucket has to be one.
|
|
214
|
+
errors.push(...validatePlatformObjectStorageBucketName(valueArg.bucketName, `${pathArg} bucketName`));
|
|
215
|
+
}
|
|
216
|
+
if (valueArg.retention !== null) {
|
|
217
|
+
errors.push(...validateObjectStorageRetentionIntent(valueArg.retention, `${pathArg} retention`));
|
|
218
|
+
}
|
|
219
|
+
return errors;
|
|
220
|
+
}
|
|
221
|
+
return [`${pathArg} capability must be database or objectstorage`];
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const validateServices = (valueArg: unknown, pathArg: string): string[] => {
|
|
225
|
+
if (!Array.isArray(valueArg) || valueArg.length > corestoreLegacyAdoptionContract.maximumServices) {
|
|
226
|
+
return [`${pathArg} must be a bounded array`];
|
|
227
|
+
}
|
|
228
|
+
const errors: string[] = [];
|
|
229
|
+
let previousServiceId: string | undefined;
|
|
230
|
+
for (const [index, service] of valueArg.entries()) {
|
|
231
|
+
const servicePath = `${pathArg}[${index}]`;
|
|
232
|
+
if (!isPlainRecord(service) || !hasExactKeys(service, ['serviceId', 'resources'])) {
|
|
233
|
+
errors.push(`${servicePath} must use its exact schema`);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (!isIdentifier(service.serviceId)) {
|
|
237
|
+
errors.push(`${servicePath} serviceId must be a canonical identifier`);
|
|
238
|
+
} else {
|
|
239
|
+
if (previousServiceId !== undefined && compareWireTokens(previousServiceId, service.serviceId) >= 0) {
|
|
240
|
+
errors.push(`${pathArg} must be sorted by serviceId without repeats`);
|
|
241
|
+
}
|
|
242
|
+
previousServiceId = service.serviceId;
|
|
243
|
+
}
|
|
244
|
+
if (!Array.isArray(service.resources) || service.resources.length < 1 || service.resources.length > 2) {
|
|
245
|
+
errors.push(`${servicePath} resources must list one or two resources`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
for (const [resourceIndex, resource] of service.resources.entries()) {
|
|
249
|
+
errors.push(...validateResource(resource, `${servicePath} resources[${resourceIndex}]`));
|
|
250
|
+
}
|
|
251
|
+
if (service.resources.length === 2
|
|
252
|
+
&& !(isPlainRecord(service.resources[0]) && service.resources[0].capability === 'database'
|
|
253
|
+
&& isPlainRecord(service.resources[1]) && service.resources[1].capability === 'objectstorage')) {
|
|
254
|
+
errors.push(`${servicePath} resources must hold one database before one objectstorage resource`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return errors;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/** Validates Corestore's answer on its legacy inventory path and names every reason it is refused. */
|
|
261
|
+
export const validateCorestoreLegacyResourceInventory = (
|
|
262
|
+
valueArg: unknown,
|
|
263
|
+
pathArg = 'corestore legacy resource inventory',
|
|
264
|
+
): string[] => {
|
|
265
|
+
if (!isPlainRecord(valueArg) || !hasExactKeys(valueArg, ['services'])) {
|
|
266
|
+
return [`${pathArg} must use its exact schema`];
|
|
267
|
+
}
|
|
268
|
+
return validateServices(valueArg.services, `${pathArg} services`);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/** Validates one node's legacy inventory as the relay reports it. Shape only. */
|
|
272
|
+
export const validateClusterCorestoreLegacyResourceInventory = (
|
|
273
|
+
valueArg: unknown,
|
|
274
|
+
pathArg = 'cluster corestore legacy resource inventory',
|
|
275
|
+
): string[] => {
|
|
276
|
+
if (!isPlainRecord(valueArg)) return [`${pathArg} must use its exact schema`];
|
|
277
|
+
const reachable = valueArg.reachable;
|
|
278
|
+
if (reachable === true
|
|
279
|
+
? !hasExactKeys(valueArg, ['nodeId', 'checkedAt', 'reachable', 'services'])
|
|
280
|
+
: reachable !== false || !hasExactKeys(valueArg, ['nodeId', 'checkedAt', 'reachable', 'errorCode'])) {
|
|
281
|
+
return [`${pathArg} must use its exact schema`];
|
|
282
|
+
}
|
|
283
|
+
const errors: string[] = [];
|
|
284
|
+
if (!isClusterNodeId(valueArg.nodeId)) errors.push(`${pathArg} nodeId must be a node id`);
|
|
285
|
+
if (!Number.isSafeInteger(valueArg.checkedAt) || (valueArg.checkedAt as number) < 0) {
|
|
286
|
+
errors.push(`${pathArg} checkedAt must be a non-negative safe integer`);
|
|
287
|
+
}
|
|
288
|
+
if (reachable === true) {
|
|
289
|
+
errors.push(...validateServices(valueArg.services, `${pathArg} services`));
|
|
290
|
+
} else if (valueArg.errorCode !== 'CORESTORE_ENDPOINT_UNKNOWN' && valueArg.errorCode !== 'CORESTORE_UNREACHABLE') {
|
|
291
|
+
errors.push(`${pathArg} errorCode must name why the node could not be read`);
|
|
292
|
+
}
|
|
293
|
+
return errors;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const failDigest = (reasonArg: string): never => {
|
|
297
|
+
throw new Error(reasonArg);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The digest an adoption is checked against: the node and its services, not the read time, so two
|
|
302
|
+
* reads of an unchanged Corestore agree. Throws for an invalid or unreachable inventory.
|
|
303
|
+
*/
|
|
304
|
+
export const computeClusterCorestoreLegacyResourceInventorySha256 = async (
|
|
305
|
+
inventoryArg: TClusterCorestoreLegacyResourceInventory,
|
|
306
|
+
): Promise<string> => {
|
|
307
|
+
const errors = validateClusterCorestoreLegacyResourceInventory(inventoryArg);
|
|
308
|
+
if (errors.length > 0) throw new Error(errors[0]);
|
|
309
|
+
if (inventoryArg.reachable !== true) throw new Error('an unreachable legacy inventory has no digest');
|
|
310
|
+
return createCanonicalJsonSha256Hex(canonicalizeStrictJson({
|
|
311
|
+
domain: corestoreLegacyAdoptionContract.digestDomain,
|
|
312
|
+
nodeId: inventoryArg.nodeId,
|
|
313
|
+
services: inventoryArg.services,
|
|
314
|
+
}, failDigest, 'corestore legacy resource inventory'), failDigest);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const selectionKey = (entryArg: { serviceId: string; capability: string }): string =>
|
|
318
|
+
`${entryArg.serviceId}\u0000${entryArg.capability}`;
|
|
319
|
+
|
|
320
|
+
/** Validates an administrator's selection: 1..4096 exact entries, sorted by service then capability. */
|
|
321
|
+
export const validateCorestoreLegacyResourceSelections = (
|
|
322
|
+
valueArg: unknown,
|
|
323
|
+
pathArg = 'corestore legacy resource selection',
|
|
324
|
+
): string[] => {
|
|
325
|
+
if (!Array.isArray(valueArg) || valueArg.length < 1
|
|
326
|
+
|| valueArg.length > corestoreLegacyAdoptionContract.maximumPlanEntries) {
|
|
327
|
+
return [`${pathArg} must be a non-empty bounded array`];
|
|
328
|
+
}
|
|
329
|
+
const errors: string[] = [];
|
|
330
|
+
let previous: string | undefined;
|
|
331
|
+
for (const [index, entry] of valueArg.entries()) {
|
|
332
|
+
if (!isPlainRecord(entry) || !hasExactKeys(entry, ['serviceId', 'capability'])
|
|
333
|
+
|| !isIdentifier(entry.serviceId) || !isCapability(entry.capability)) {
|
|
334
|
+
errors.push(`${pathArg}[${index}] must be an exact { serviceId, capability }`);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const key = selectionKey(entry as unknown as ICorestoreLegacyResourceSelection);
|
|
338
|
+
if (previous !== undefined && compareWireTokens(previous, key) >= 0) {
|
|
339
|
+
errors.push(`${pathArg} must be sorted by serviceId then capability without repeats`);
|
|
340
|
+
}
|
|
341
|
+
previous = key;
|
|
342
|
+
}
|
|
343
|
+
return errors;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Validates an adoption plan by the rules Corestore's migration applies: a non-empty array of at
|
|
348
|
+
* most 4096 exact `{ serviceId, capability, bindingId }` entries, canonical identifiers, and no
|
|
349
|
+
* service capability or binding id twice. Order is free.
|
|
350
|
+
*/
|
|
351
|
+
export const validateCorestoreLegacyResourceAdoptionPlan = (
|
|
352
|
+
valueArg: unknown,
|
|
353
|
+
pathArg = 'adoption plan',
|
|
354
|
+
): string[] => {
|
|
355
|
+
if (!Array.isArray(valueArg) || valueArg.length === 0
|
|
356
|
+
|| valueArg.length > corestoreLegacyAdoptionContract.maximumPlanEntries) {
|
|
357
|
+
return [`${pathArg} must be a non-empty bounded array`];
|
|
358
|
+
}
|
|
359
|
+
const errors: string[] = [];
|
|
360
|
+
const serviceCapabilities = new Set<string>();
|
|
361
|
+
const bindingIds = new Set<string>();
|
|
362
|
+
for (const [index, entry] of valueArg.entries()) {
|
|
363
|
+
if (!isPlainRecord(entry) || !hasExactKeys(entry, ['serviceId', 'capability', 'bindingId'])
|
|
364
|
+
|| !isIdentifier(entry.serviceId) || !isIdentifier(entry.bindingId) || !isCapability(entry.capability)) {
|
|
365
|
+
errors.push(`${pathArg} entry ${index} must be an exact { serviceId, capability, bindingId }`);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const serviceCapability = selectionKey(entry as unknown as ICorestoreLegacyResourceSelection);
|
|
369
|
+
if (serviceCapabilities.has(serviceCapability) || bindingIds.has(entry.bindingId as string)) {
|
|
370
|
+
errors.push(`${pathArg} entry ${index} repeats a service capability or binding id`);
|
|
371
|
+
}
|
|
372
|
+
serviceCapabilities.add(serviceCapability);
|
|
373
|
+
bindingIds.add(entry.bindingId as string);
|
|
374
|
+
}
|
|
375
|
+
return errors;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
/** The binding fields an adoption judges; a full `IPlatformBinding` satisfies it. */
|
|
379
|
+
export interface ICorestoreLegacyAdoptionBindingView {
|
|
380
|
+
id: string;
|
|
381
|
+
serviceId: string;
|
|
382
|
+
capability: string;
|
|
383
|
+
objectstorageBucketName?: string;
|
|
384
|
+
objectstorageRetention?: { intent: IObjectStorageRetentionIntent };
|
|
385
|
+
corestoreLegacyAdoption?: { nodeId: string; resourceName: string; inventorySha256: string };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Content check of one adopting binding against the inventory it was adopted at, and the one rule
|
|
390
|
+
* that lets a binding carry a legacy bucket name: the binding names this node, this inventory
|
|
391
|
+
* digest and the legacy resource's name; the resource is unadopted without a blocker or authorized
|
|
392
|
+
* for exactly this binding; an adopted bucket's `objectstorageBucketName` is the legacy bucket and
|
|
393
|
+
* its retention intent the legacy intent exactly, absent when the legacy bucket has none; a
|
|
394
|
+
* database binding carries neither. Whether the digest is current is the caller's to check.
|
|
395
|
+
*/
|
|
396
|
+
export const validatePlatformBindingCorestoreLegacyAdoption = (
|
|
397
|
+
bindingArg: ICorestoreLegacyAdoptionBindingView,
|
|
398
|
+
inventoryArg: TClusterCorestoreLegacyResourceInventory,
|
|
399
|
+
inventorySha256Arg: string,
|
|
400
|
+
pathArg = 'platform binding corestoreLegacyAdoption',
|
|
401
|
+
): string[] => {
|
|
402
|
+
if (validateClusterCorestoreLegacyResourceInventory(inventoryArg).length > 0 || inventoryArg.reachable !== true
|
|
403
|
+
|| typeof inventorySha256Arg !== 'string' || !bareSha256Pattern.test(inventorySha256Arg)) {
|
|
404
|
+
return [`${pathArg} needs a valid reachable inventory and its digest`];
|
|
405
|
+
}
|
|
406
|
+
const adoption = bindingArg.corestoreLegacyAdoption;
|
|
407
|
+
if (!isPlainRecord(adoption) || !hasExactKeys(adoption, ['nodeId', 'resourceName', 'inventorySha256'])) {
|
|
408
|
+
return [`${pathArg} must use its exact schema`];
|
|
409
|
+
}
|
|
410
|
+
if (adoption.nodeId !== inventoryArg.nodeId || adoption.inventorySha256 !== inventorySha256Arg) {
|
|
411
|
+
return [`${pathArg} must name the inventory's node and digest`];
|
|
412
|
+
}
|
|
413
|
+
if (!isCapability(bindingArg.capability)) return [`${pathArg} needs a database or objectstorage binding`];
|
|
414
|
+
const resource = inventoryArg.services.find((serviceArg) => serviceArg.serviceId === bindingArg.serviceId)
|
|
415
|
+
?.resources.find((resourceArg) => resourceArg.capability === bindingArg.capability);
|
|
416
|
+
if (!resource) return [`${pathArg} names no legacy resource of the service and capability`];
|
|
417
|
+
const errors: string[] = [];
|
|
418
|
+
const legacyName = resource.capability === 'database' ? resource.databaseName : resource.bucketName;
|
|
419
|
+
if (adoption.resourceName !== legacyName) errors.push(`${pathArg} resourceName must be the legacy resource's name`);
|
|
420
|
+
if (resource.adoption.state === 'unadopted' ? resource.adoption.blocker !== null
|
|
421
|
+
: resource.adoption.bindingId !== bindingArg.id) {
|
|
422
|
+
errors.push(`${pathArg} names a resource that is blocked or authorized for another binding`);
|
|
423
|
+
}
|
|
424
|
+
if (resource.capability === 'objectstorage') {
|
|
425
|
+
if (bindingArg.objectstorageBucketName !== resource.bucketName) {
|
|
426
|
+
errors.push(`${pathArg} objectstorageBucketName must be the legacy bucket`);
|
|
427
|
+
}
|
|
428
|
+
if (resource.retention === null
|
|
429
|
+
? bindingArg.objectstorageRetention !== undefined
|
|
430
|
+
: !objectStorageRetentionIntentsEqual(bindingArg.objectstorageRetention?.intent, resource.retention)) {
|
|
431
|
+
errors.push(`${pathArg} retention intent must be the legacy bucket's exactly`);
|
|
432
|
+
}
|
|
433
|
+
} else if (bindingArg.objectstorageBucketName !== undefined || bindingArg.objectstorageRetention !== undefined) {
|
|
434
|
+
errors.push(`${pathArg} database binding must carry no bucket or retention`);
|
|
435
|
+
}
|
|
436
|
+
return errors;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* The plan for every adopting binding, sorted by service then capability, in the exact entry shape
|
|
441
|
+
* Corestore's migration reads. Throws unless every binding adopts a legacy resource and the result
|
|
442
|
+
* is a valid plan.
|
|
443
|
+
*/
|
|
444
|
+
export const createCorestoreLegacyResourceAdoptionPlan = (
|
|
445
|
+
bindingsArg: readonly ICorestoreLegacyAdoptionBindingView[],
|
|
446
|
+
): ICorestoreLegacyResourceAdoptionPlanEntry[] => {
|
|
447
|
+
const plan = bindingsArg.map((bindingArg) => {
|
|
448
|
+
if (bindingArg.corestoreLegacyAdoption === undefined || !isCapability(bindingArg.capability)) {
|
|
449
|
+
throw new Error(`binding ${bindingArg.id} adopts no legacy Corestore resource`);
|
|
450
|
+
}
|
|
451
|
+
return { serviceId: bindingArg.serviceId, capability: bindingArg.capability, bindingId: bindingArg.id };
|
|
452
|
+
}).sort((leftArg, rightArg) => compareWireTokens(selectionKey(leftArg), selectionKey(rightArg)));
|
|
453
|
+
const errors = validateCorestoreLegacyResourceAdoptionPlan(plan);
|
|
454
|
+
if (errors.length > 0) throw new Error(errors[0]);
|
|
455
|
+
return plan;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* The binding status reason a Corestore control answer names: a 409 whose body is
|
|
460
|
+
* `{ ok: false, error, code }` with `code` one of the legacy adoption reasons. Every other answer,
|
|
461
|
+
* including a 409 with another code, names none and keeps its ordinary failure handling.
|
|
462
|
+
*/
|
|
463
|
+
export const readCorestoreLegacyAdoptionRefusal = (
|
|
464
|
+
statusCodeArg: number,
|
|
465
|
+
bodyArg: unknown,
|
|
466
|
+
): TPlatformBindingStatusReason | null => (
|
|
467
|
+
statusCodeArg === 409 && isPlainRecord(bodyArg) && bodyArg.ok === false
|
|
468
|
+
&& (platformBindingStatusReasons as readonly unknown[]).includes(bodyArg.code)
|
|
469
|
+
? bodyArg.code as TPlatformBindingStatusReason
|
|
470
|
+
: null
|
|
471
|
+
);
|
package/ts/data/index.ts
CHANGED
|
@@ -3,9 +3,11 @@ export * from './cluster.js';
|
|
|
3
3
|
export * from './clusterruntime.js';
|
|
4
4
|
export * from './clustervpn.js';
|
|
5
5
|
export * from './clusteringress.js';
|
|
6
|
+
export * from './clusteringressdesignation.js';
|
|
6
7
|
export * from './clusterrelaycredential.js';
|
|
7
8
|
export * from './config.js';
|
|
8
9
|
export * from './corestore.js';
|
|
10
|
+
export * from './corestorelegacyadoption.js';
|
|
9
11
|
export * from './coremail.js';
|
|
10
12
|
export * from './coremail.runtime.js';
|
|
11
13
|
export * from './deployment.js';
|
|
@@ -40,6 +42,9 @@ export * from './networkprojection.js';
|
|
|
40
42
|
export type { IRuntimeNetworkRouterPacketGrant, IRuntimeNetworkRouterProjection } from './networkrouter.js';
|
|
41
43
|
export type { IRuntimeNetworkHostIngress, IRuntimeNetworkHostPacketGrant, IRuntimeNetworkHostProjection } from './networkhost.js';
|
|
42
44
|
export { runtimeNetworkHostContract } from './networkhost.js';
|
|
45
|
+
export type { IRuntimeNetworkWorkloadIngress, IRuntimeNetworkWorkloadIngressPacketGrant,
|
|
46
|
+
IRuntimeNetworkWorkloadIngressProjection } from './networkworkloadingress.js';
|
|
47
|
+
export { runtimeNetworkWorkloadIngressContract } from './networkworkloadingress.js';
|
|
43
48
|
export * from './networkprotection.js';
|
|
44
49
|
export * from './networksigning.js';
|
|
45
50
|
export * from './privatenetwork.js';
|
|
@@ -48,6 +53,7 @@ export * from './releaseversion.js';
|
|
|
48
53
|
export * from './runtimeassignment.js';
|
|
49
54
|
export * from './runtimeconfig.js';
|
|
50
55
|
export * from './runtimestorageclaim.js';
|
|
56
|
+
export * from './runtimelocalstorageclaim.js';
|
|
51
57
|
export * from './runtimesession.js';
|
|
52
58
|
export * from './runtimeterminalreceipt.js';
|
|
53
59
|
export * from './secret.js';
|
|
@@ -62,6 +68,7 @@ export * from './service.js';
|
|
|
62
68
|
export * from './servicecredential.js';
|
|
63
69
|
export * from './serviceports.js';
|
|
64
70
|
export * from './serviceruntime.js';
|
|
71
|
+
export * from './servicevolumeinitialization.js';
|
|
65
72
|
export * from './status.js';
|
|
66
73
|
export * from './taskexecution.js';
|
|
67
74
|
export * from './traffic.js';
|
|
@@ -9,6 +9,8 @@ import { routerPacketGrantsOf, snapshotRouterProjection,
|
|
|
9
9
|
type IRuntimeNetworkRouterPacketGrant, type IRuntimeNetworkRouterProjection } from './networkrouter.js';
|
|
10
10
|
import { hostPacketGrantsOf, snapshotHostProjection,
|
|
11
11
|
type IRuntimeNetworkHostPacketGrant, type IRuntimeNetworkHostProjection } from './networkhost.js';
|
|
12
|
+
import { snapshotWorkloadIngressProjection, workloadIngressPacketGrantsOf,
|
|
13
|
+
type IRuntimeNetworkWorkloadIngressPacketGrant, type IRuntimeNetworkWorkloadIngressProjection } from './networkworkloadingress.js';
|
|
12
14
|
import { buildPrivateNetworkFqdn, composeServicePrivateNetworkDnsPolicy,
|
|
13
15
|
snapshotPrivateNetwork, snapshotServicePrivateNetworks } from './privatenetwork.js';
|
|
14
16
|
import { captureNetwork, failNetwork, networkCanonical, networkContains, networkCounter, networkDigest,
|
|
@@ -167,6 +169,21 @@ export interface IRuntimeNetworkProjection extends IRuntimeNetworkNodeScope {
|
|
|
167
169
|
* host-origin flow; any other controller kind carrying it is refused.
|
|
168
170
|
*/
|
|
169
171
|
host?: IRuntimeNetworkHostProjection;
|
|
172
|
+
/**
|
|
173
|
+
* One-way workload ingress selections of a `cloudly` projection: the cluster ingress workload dials
|
|
174
|
+
* exact target workload ports across organizations (`IRuntimeNetworkWorkloadIngressPacketGrant`).
|
|
175
|
+
* Omitted, it keeps the canonical bytes and grants no such flow; any other controller kind
|
|
176
|
+
* carrying it is refused.
|
|
177
|
+
*/
|
|
178
|
+
workloadIngress?: IRuntimeNetworkWorkloadIngressProjection;
|
|
179
|
+
/**
|
|
180
|
+
* Ids of the protected authority's platform endpoints whose address this projection's node's own
|
|
181
|
+
* host carries, such as a relay listener or a Corestore beside the node on one machine. The node
|
|
182
|
+
* delivers workload flows to them locally instead of forwarding them. Sorted, unique and never
|
|
183
|
+
* empty when present; omitted, it keeps the canonical bytes and states that no endpoint is local.
|
|
184
|
+
* It grants nothing: a workload still reaches only the endpoints it selects.
|
|
185
|
+
*/
|
|
186
|
+
hostPlatformEndpointIds?: string[];
|
|
170
187
|
digest: TSha256Digest;
|
|
171
188
|
}
|
|
172
189
|
|
|
@@ -322,9 +339,16 @@ export const snapshotRuntimeNetworkProjection = (value: unknown): IRuntimeNetwor
|
|
|
322
339
|
requireNetwork(new TextEncoder().encode(networkCanonical(captured)).length <= runtimeNetworkProjectionContract.maximumCanonicalBytes);
|
|
323
340
|
const hasRouter = captured !== null && typeof captured === 'object' && Object.hasOwn(captured, 'router');
|
|
324
341
|
const hasHost = captured !== null && typeof captured === 'object' && Object.hasOwn(captured, 'host');
|
|
342
|
+
const hasWorkloadIngress = captured !== null && typeof captured === 'object' && Object.hasOwn(captured, 'workloadIngress');
|
|
343
|
+
const hasHostPlatformEndpoints = captured !== null && typeof captured === 'object'
|
|
344
|
+
&& Object.hasOwn(captured, 'hostPlatformEndpointIds');
|
|
325
345
|
const item = networkRecord(captured, ['id', 'generation', 'previous', 'controller', 'nodeId', 'runtimeNamespace',
|
|
326
346
|
'protectedAuthority', 'allocationAuthorities', 'handoff', 'networks', 'memberships', 'endpoints', 'dns', 'withdrawals', 'retiredLeases', 'retiredHandoffs', 'digest',
|
|
327
|
-
...(hasRouter ? ['router'] : []), ...(hasHost ? ['host'] : [])])
|
|
347
|
+
...(hasRouter ? ['router'] : []), ...(hasHost ? ['host'] : []), ...(hasWorkloadIngress ? ['workloadIngress'] : []),
|
|
348
|
+
...(hasHostPlatformEndpoints ? ['hostPlatformEndpointIds'] : [])]);
|
|
349
|
+
const hostPlatformEndpointIds = hasHostPlatformEndpoints
|
|
350
|
+
? networkSorted(networkList(item.hostPlatformEndpointIds, 64, networkId), value => value) : [];
|
|
351
|
+
requireNetwork(!hasHostPlatformEndpoints || hostPlatformEndpointIds.length > 0);
|
|
328
352
|
return { ...networkRevision(item), ...networkScope(item),
|
|
329
353
|
protectedAuthority: snapshotRuntimeNetworkProtectedAuthority(item.protectedAuthority),
|
|
330
354
|
allocationAuthorities: networkSorted(networkList(item.allocationAuthorities, 16, snapshotRuntimeNetworkProtectedAuthority), refKey),
|
|
@@ -335,6 +359,8 @@ export const snapshotRuntimeNetworkProjection = (value: unknown): IRuntimeNetwor
|
|
|
335
359
|
withdrawals: networkSorted(networkList(item.withdrawals, 4096, packetGrant), networkCanonical),
|
|
336
360
|
...(hasRouter ? { router: snapshotRouterProjection(item.router) } : {}),
|
|
337
361
|
...(hasHost ? { host: snapshotHostProjection(item.host) } : {}),
|
|
362
|
+
...(hasWorkloadIngress ? { workloadIngress: snapshotWorkloadIngressProjection(item.workloadIngress) } : {}),
|
|
363
|
+
...(hasHostPlatformEndpoints ? { hostPlatformEndpointIds } : {}),
|
|
338
364
|
retiredLeases: sortedReferences(item.retiredLeases), retiredHandoffs: sortedReferences(item.retiredHandoffs), digest: networkDigest(item.digest) };
|
|
339
365
|
});
|
|
340
366
|
|
|
@@ -434,6 +460,7 @@ export const validateRuntimeNetworkProjection = async (value: unknown): Promise<
|
|
|
434
460
|
}
|
|
435
461
|
requireNetwork(!projection.retiredLeases.some(retired => networkEqual(retired, networkReferenceOf(lease))));
|
|
436
462
|
}
|
|
463
|
+
for (const id of projection.hostPlatformEndpointIds ?? []) requireNetwork(current.platformEndpoints.some(platform => platform.id === id));
|
|
437
464
|
for (let index = 0; index < projection.endpoints.length; index++) for (let previous = 0; previous < index; previous++) {
|
|
438
465
|
requireNetwork(!networkOverlaps(projection.endpoints[index].lease.ipv4.linkSubnet, projection.endpoints[previous].lease.ipv4.linkSubnet));
|
|
439
466
|
}
|
|
@@ -444,6 +471,8 @@ export const validateRuntimeNetworkProjection = async (value: unknown): Promise<
|
|
|
444
471
|
requireNetwork((projection.router?.withdrawals ?? []).every(grant => !routerGrants.has(networkCanonical(grant))));
|
|
445
472
|
const hostGrants = new Set(hostPacketGrantsOf(projection).map(networkCanonical));
|
|
446
473
|
requireNetwork((projection.host?.withdrawals ?? []).every(grant => !hostGrants.has(networkCanonical(grant))));
|
|
474
|
+
const workloadIngressGrants = new Set(workloadIngressPacketGrantsOf(projection).map(networkCanonical));
|
|
475
|
+
requireNetwork((projection.workloadIngress?.withdrawals ?? []).every(grant => !workloadIngressGrants.has(networkCanonical(grant))));
|
|
447
476
|
requireNetwork(projection.digest === await computeRuntimeNetworkProjectionDigest(projection));
|
|
448
477
|
return projection;
|
|
449
478
|
} catch { return failNetwork(); }
|
|
@@ -461,6 +490,12 @@ export const getRuntimeNetworkProjectionRouterPacketGrants = async (value: unkno
|
|
|
461
490
|
export const getRuntimeNetworkProjectionHostPacketGrants = async (value: unknown): Promise<IRuntimeNetworkHostPacketGrant[]> =>
|
|
462
491
|
hostPacketGrantsOf(await validateRuntimeNetworkProjection(value));
|
|
463
492
|
|
|
493
|
+
/** One-way workload ingress authority comes only from explicit signed `cloudly` selections of exact target ports. */
|
|
494
|
+
export const getRuntimeNetworkProjectionWorkloadIngressPacketGrants = async (
|
|
495
|
+
value: unknown,
|
|
496
|
+
): Promise<IRuntimeNetworkWorkloadIngressPacketGrant[]> =>
|
|
497
|
+
workloadIngressPacketGrantsOf(await validateRuntimeNetworkProjection(value));
|
|
498
|
+
|
|
464
499
|
/** Complete offline DNS material, including authorized names never previously queried. */
|
|
465
500
|
/** Select exact local receivers before expansion; all targets still come from
|
|
466
501
|
* the complete validated projection. Selection supplies no attachment authority. */
|
|
@@ -526,6 +561,7 @@ export const admitRuntimeNetworkProjection = async (
|
|
|
526
561
|
if (previousInput === null) {
|
|
527
562
|
requireNetwork(projection.generation === 1 && applied === null && projection.withdrawals.length === 0
|
|
528
563
|
&& (projection.router?.withdrawals.length ?? 0) === 0 && (projection.host?.withdrawals.length ?? 0) === 0
|
|
564
|
+
&& (projection.workloadIngress?.withdrawals.length ?? 0) === 0
|
|
529
565
|
&& projection.retiredLeases.length === 0 && projection.retiredHandoffs.length === 0);
|
|
530
566
|
return { status: 'accepted', projection };
|
|
531
567
|
}
|
|
@@ -567,6 +603,15 @@ export const admitRuntimeNetworkProjection = async (
|
|
|
567
603
|
for (const grant of previousHostGrants) requireNetwork(hostGrants.has(networkCanonical(grant)) || hostWithdrawals.has(networkCanonical(grant)));
|
|
568
604
|
const knownHostWithdrawals = new Set([...previousHostGrants, ...(previous.host?.withdrawals ?? [])].map(networkCanonical));
|
|
569
605
|
requireNetwork((projection.host?.withdrawals ?? []).every(grant => knownHostWithdrawals.has(networkCanonical(grant))));
|
|
606
|
+
const workloadIngressGrants = new Set(workloadIngressPacketGrantsOf(projection).map(networkCanonical));
|
|
607
|
+
const workloadIngressWithdrawals = new Set((projection.workloadIngress?.withdrawals ?? []).map(networkCanonical));
|
|
608
|
+
const previousWorkloadIngressGrants = workloadIngressPacketGrantsOf(previous);
|
|
609
|
+
for (const grant of previousWorkloadIngressGrants) requireNetwork(workloadIngressGrants.has(networkCanonical(grant))
|
|
610
|
+
|| workloadIngressWithdrawals.has(networkCanonical(grant)));
|
|
611
|
+
const knownWorkloadIngressWithdrawals = new Set([...previousWorkloadIngressGrants,
|
|
612
|
+
...(previous.workloadIngress?.withdrawals ?? [])].map(networkCanonical));
|
|
613
|
+
requireNetwork((projection.workloadIngress?.withdrawals ?? []).every(grant =>
|
|
614
|
+
knownWorkloadIngressWithdrawals.has(networkCanonical(grant))));
|
|
570
615
|
const knownLeases = [...previous.endpoints.map(endpoint => networkReferenceOf(endpoint.lease)), ...previous.retiredLeases];
|
|
571
616
|
const knownHandoffs = [...(previous.handoff ? [networkReferenceOf(previous.handoff)] : []), ...previous.retiredHandoffs];
|
|
572
617
|
requireNetwork(projection.retiredLeases.every(retired => knownLeases.some(known => networkEqual(known, retired)))
|
|
@@ -578,6 +623,9 @@ export const admitRuntimeNetworkProjection = async (
|
|
|
578
623
|
for (const grant of previous.withdrawals) requireNetwork(withdrawn.has(networkCanonical(grant)));
|
|
579
624
|
for (const grant of previous.router?.withdrawals ?? []) requireNetwork(routerWithdrawals.has(networkCanonical(grant)));
|
|
580
625
|
for (const grant of previous.host?.withdrawals ?? []) requireNetwork(hostWithdrawals.has(networkCanonical(grant)));
|
|
626
|
+
for (const grant of previous.workloadIngress?.withdrawals ?? []) {
|
|
627
|
+
requireNetwork(workloadIngressWithdrawals.has(networkCanonical(grant)));
|
|
628
|
+
}
|
|
581
629
|
requireCarried(previous.retiredLeases, projection.retiredLeases); requireCarried(previous.retiredHandoffs, projection.retiredHandoffs);
|
|
582
630
|
}
|
|
583
631
|
for (const old of previous.endpoints) {
|