@serve.zone/interfaces 32.20.1 → 32.22.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 +74 -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/gateway.d.ts +169 -0
- package/dist_ts/data/gateway.js +115 -2
- package/dist_ts/data/index.d.ts +4 -0
- package/dist_ts/data/index.js +4 -1
- package/dist_ts/data/networkprojection.d.ts +18 -0
- package/dist_ts/data/networkprojection.js +31 -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/serviceruntime.d.ts +10 -3
- package/dist_ts/data/serviceruntime.js +6 -2
- package/dist_ts/private/ipaddress.d.ts +29 -0
- package/dist_ts/private/ipaddress.js +120 -0
- 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/gateway.d.ts +58 -1
- package/dist_ts/requests/runtimesession.d.ts +12 -0
- package/package.json +1 -1
- package/readme.md +167 -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/gateway.ts +308 -0
- package/ts/data/index.ts +5 -0
- package/ts/data/networkprojection.ts +49 -1
- package/ts/data/networkworkloadingress.ts +107 -0
- package/ts/data/runtimelocalstorageclaim.ts +275 -0
- package/ts/data/serviceruntime.ts +13 -2
- package/ts/private/ipaddress.ts +140 -0
- package/ts/private/storagecontract.ts +1 -0
- package/ts/requests/clusteringress.ts +65 -0
- package/ts/requests/gateway.ts +71 -0
- package/ts/requests/runtimesession.ts +16 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { TSha256Digest } from './immutableimage.js';
|
|
2
|
+
import type { IRuntimeAssignment, IRuntimeAuthorityReference, IRuntimeController } from './runtimeassignment.js';
|
|
3
|
+
import { snapshotRuntimeAssignment, validateRuntimeAssignment } from './runtimeassignment.js';
|
|
4
|
+
import type { IRuntimeSessionBinding } from './runtimesession.js';
|
|
5
|
+
import { snapshotRuntimeSessionBinding } from './runtimesession.js';
|
|
6
|
+
import { captureNetwork, networkController, networkDigest, networkEqual, networkHash, networkId,
|
|
7
|
+
networkNodeId, networkRecord, networkReference, networkReferenceOf, networkRevision, requireNetwork,
|
|
8
|
+
} from '../private/networkcontract.js';
|
|
9
|
+
import { isRuntimeIdentityNumber } from '../private/runtimepolicy.js';
|
|
10
|
+
import { createStorageContractReaders } from '../private/storagecontract.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Where a local storage claim's bytes live, named by kind and never by path.
|
|
14
|
+
*
|
|
15
|
+
* `directory` is a node-local directory the node derives from the claim's identity under a root
|
|
16
|
+
* only the node knows. Network filesystems join this union as further kinds; their credentials
|
|
17
|
+
* travel as secrets and never inside a claim.
|
|
18
|
+
*/
|
|
19
|
+
export type TRuntimeLocalStorageSource = { kind: 'directory' };
|
|
20
|
+
|
|
21
|
+
/** The owner and permission bits a node gives an `empty` volume's root directory when it creates it. */
|
|
22
|
+
export interface IRuntimeLocalStorageOwnership {
|
|
23
|
+
uid: number;
|
|
24
|
+
gid: number;
|
|
25
|
+
/** Numeric directory permission bits, for example `0o750`; setuid is refused. */
|
|
26
|
+
mode: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Where the volume appears inside the workload's container. */
|
|
30
|
+
export interface IRuntimeLocalStorageMount {
|
|
31
|
+
mountPath: string;
|
|
32
|
+
readOnly: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `bound` keeps the volume and lets the node mount it; `purge` orders the node to delete the
|
|
37
|
+
* volume's bytes. A purge always follows a bound generation of the same claim.
|
|
38
|
+
*/
|
|
39
|
+
export type TRuntimeLocalStorageLifecycle = 'bound' | 'purge';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A controller's authority for one retained, node-local volume of one service.
|
|
43
|
+
*
|
|
44
|
+
* Cloudly and Onebox issue it alike. It names the volume by identity only: no host path, no
|
|
45
|
+
* device, no mount option and no credential. The node resolves the identity to its own storage.
|
|
46
|
+
*
|
|
47
|
+
* `initialization` says what the node does when the volume does not exist yet. `empty` creates an
|
|
48
|
+
* empty root with `initialOwnership`, which it must state, so a workload that runs as the image's
|
|
49
|
+
* own user never meets a root-owned directory by accident. `import` waits for the volume's bytes to
|
|
50
|
+
* be imported and states no ownership: an imported tree keeps the ownership it carries.
|
|
51
|
+
*/
|
|
52
|
+
export interface IRuntimeLocalStorageClaim {
|
|
53
|
+
id: string;
|
|
54
|
+
generation: number;
|
|
55
|
+
previous: IRuntimeAuthorityReference | null;
|
|
56
|
+
controller: IRuntimeController;
|
|
57
|
+
organizationId: string;
|
|
58
|
+
clusterId: string;
|
|
59
|
+
nodeId: string;
|
|
60
|
+
runtimeNamespace: string;
|
|
61
|
+
serviceId: string;
|
|
62
|
+
/** The service's own name for this volume, stable across every generation of the claim. */
|
|
63
|
+
volumeId: string;
|
|
64
|
+
source: TRuntimeLocalStorageSource;
|
|
65
|
+
initialization: 'empty' | 'import';
|
|
66
|
+
/** Required for `empty`, `null` for `import`. */
|
|
67
|
+
initialOwnership: IRuntimeLocalStorageOwnership | null;
|
|
68
|
+
mount: IRuntimeLocalStorageMount;
|
|
69
|
+
accessMode: 'ReadWriteOnce';
|
|
70
|
+
reclaimPolicy: 'retain';
|
|
71
|
+
lifecycle: TRuntimeLocalStorageLifecycle;
|
|
72
|
+
digest: TSha256Digest;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What the node holds for the volume after admission.
|
|
77
|
+
*
|
|
78
|
+
* `ready` exists and may be mounted; `import-required` is an `import` volume whose bytes have not
|
|
79
|
+
* arrived; `purged` is a volume whose bytes the node deleted; `uncertain` is a volume the node
|
|
80
|
+
* cannot vouch for, which it neither mounts nor deletes until an operator decides.
|
|
81
|
+
*/
|
|
82
|
+
export type TRuntimeLocalStorageClaimState = 'ready' | 'import-required' | 'purged' | 'uncertain';
|
|
83
|
+
|
|
84
|
+
/** Controller push over the node's current authenticated session. */
|
|
85
|
+
export interface IApplyRuntimeLocalStorageClaimRequest {
|
|
86
|
+
session: IRuntimeSessionBinding;
|
|
87
|
+
claim: IRuntimeLocalStorageClaim;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Durable admission acknowledgement. `claim` is the reference of the claim that was sent, also on
|
|
92
|
+
* a `historical` answer, whose `state` describes the newer generation the node holds.
|
|
93
|
+
*/
|
|
94
|
+
export interface IApplyRuntimeLocalStorageClaimResponse {
|
|
95
|
+
status: 'accepted' | 'replay' | 'historical';
|
|
96
|
+
state: TRuntimeLocalStorageClaimState;
|
|
97
|
+
claim: IRuntimeAuthorityReference;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const runtimeLocalStorageClaimContract = Object.freeze({
|
|
101
|
+
digestDomain: 'serve.zone/runtime-local-storage-claim',
|
|
102
|
+
error: 'Runtime local storage claim contract rejected.',
|
|
103
|
+
accessMode: 'ReadWriteOnce',
|
|
104
|
+
reclaimPolicy: 'retain',
|
|
105
|
+
/** Every permission bit a claim may state: setgid, sticky and the rwx triplets. */
|
|
106
|
+
directoryModeBits: 0o3777,
|
|
107
|
+
} as const);
|
|
108
|
+
|
|
109
|
+
const fail = (): never => { throw new Error(runtimeLocalStorageClaimContract.error); };
|
|
110
|
+
const safe = <T>(operationArg: () => T): T => { try { return operationArg(); } catch { return fail(); } };
|
|
111
|
+
const { readMountPath } = createStorageContractReaders(fail);
|
|
112
|
+
const claimStates = ['ready', 'import-required', 'purged', 'uncertain'] as const;
|
|
113
|
+
const responseStatuses = ['accepted', 'replay', 'historical'] as const;
|
|
114
|
+
|
|
115
|
+
const sourceOf = (valueArg: unknown): TRuntimeLocalStorageSource => {
|
|
116
|
+
const value = networkRecord(valueArg, ['kind']);
|
|
117
|
+
requireNetwork(value.kind === 'directory');
|
|
118
|
+
return { kind: 'directory' };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const ownershipOf = (valueArg: unknown): IRuntimeLocalStorageOwnership => {
|
|
122
|
+
const value = networkRecord(valueArg, ['uid', 'gid', 'mode']);
|
|
123
|
+
requireNetwork(isRuntimeIdentityNumber(value.uid) && isRuntimeIdentityNumber(value.gid)
|
|
124
|
+
&& Number.isSafeInteger(value.mode) && (value.mode as number) >= 0
|
|
125
|
+
&& ((value.mode as number) & ~runtimeLocalStorageClaimContract.directoryModeBits) === 0);
|
|
126
|
+
return { uid: value.uid as number, gid: value.gid as number, mode: value.mode as number };
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const mountOf = (valueArg: unknown): IRuntimeLocalStorageMount => {
|
|
130
|
+
const value = networkRecord(valueArg, ['mountPath', 'readOnly']);
|
|
131
|
+
requireNetwork(typeof value.readOnly === 'boolean');
|
|
132
|
+
return { mountPath: readMountPath(value.mountPath, 'mountPath'), readOnly: value.readOnly };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Detached exact-schema snapshot. The digest is a separate check. */
|
|
136
|
+
export const snapshotRuntimeLocalStorageClaim = (valueArg: unknown): IRuntimeLocalStorageClaim => safe(() => {
|
|
137
|
+
const value = networkRecord(captureNetwork(valueArg), ['id', 'generation', 'previous', 'controller',
|
|
138
|
+
'organizationId', 'clusterId', 'nodeId', 'runtimeNamespace', 'serviceId', 'volumeId', 'source',
|
|
139
|
+
'initialization', 'initialOwnership', 'mount', 'accessMode', 'reclaimPolicy', 'lifecycle', 'digest']);
|
|
140
|
+
const { id, generation, previous } = networkRevision(value);
|
|
141
|
+
requireNetwork((value.initialization === 'empty' && value.initialOwnership !== null)
|
|
142
|
+
|| (value.initialization === 'import' && value.initialOwnership === null));
|
|
143
|
+
requireNetwork(value.accessMode === 'ReadWriteOnce' && value.reclaimPolicy === 'retain');
|
|
144
|
+
requireNetwork(value.lifecycle === 'bound' || (value.lifecycle === 'purge' && previous !== null));
|
|
145
|
+
return {
|
|
146
|
+
id, generation, previous,
|
|
147
|
+
controller: networkController(value.controller),
|
|
148
|
+
organizationId: networkId(value.organizationId),
|
|
149
|
+
clusterId: networkId(value.clusterId),
|
|
150
|
+
nodeId: networkNodeId(value.nodeId),
|
|
151
|
+
runtimeNamespace: networkId(value.runtimeNamespace),
|
|
152
|
+
serviceId: networkId(value.serviceId),
|
|
153
|
+
volumeId: networkId(value.volumeId),
|
|
154
|
+
source: sourceOf(value.source),
|
|
155
|
+
initialization: value.initialization as IRuntimeLocalStorageClaim['initialization'],
|
|
156
|
+
initialOwnership: value.initialOwnership === null ? null : ownershipOf(value.initialOwnership),
|
|
157
|
+
mount: mountOf(value.mount),
|
|
158
|
+
accessMode: 'ReadWriteOnce',
|
|
159
|
+
reclaimPolicy: 'retain',
|
|
160
|
+
lifecycle: value.lifecycle as TRuntimeLocalStorageLifecycle,
|
|
161
|
+
digest: networkDigest(value.digest),
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export const computeRuntimeLocalStorageClaimDigest = async (valueArg: unknown): Promise<TSha256Digest> => {
|
|
166
|
+
const claim = snapshotRuntimeLocalStorageClaim(valueArg);
|
|
167
|
+
try {
|
|
168
|
+
return await networkHash(runtimeLocalStorageClaimContract.digestDomain, claim);
|
|
169
|
+
} catch {
|
|
170
|
+
return fail();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const validateRuntimeLocalStorageClaim = async (valueArg: unknown): Promise<string[]> => {
|
|
175
|
+
try {
|
|
176
|
+
const claim = snapshotRuntimeLocalStorageClaim(valueArg);
|
|
177
|
+
return claim.digest === await computeRuntimeLocalStorageClaimDigest(claim)
|
|
178
|
+
? [] : ['runtime local storage claim digest does not match'];
|
|
179
|
+
} catch {
|
|
180
|
+
return ['runtime local storage claim must use its exact canonical schema'];
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** The `{ id, generation, digest }` an assignment's `workload.storage` seals for this claim. */
|
|
185
|
+
export const getRuntimeLocalStorageClaimReference = (valueArg: unknown): IRuntimeAuthorityReference =>
|
|
186
|
+
networkReferenceOf(snapshotRuntimeLocalStorageClaim(valueArg));
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Content binding only: the assignment seals this exact claim, and both name the same controller,
|
|
190
|
+
* organization, node, runtime namespace and service. A purge claim binds to no assignment, because
|
|
191
|
+
* no workload may mount a volume being deleted. The cluster ID must come from the authenticated
|
|
192
|
+
* session, because an assignment does not contain one.
|
|
193
|
+
*/
|
|
194
|
+
export const bindRuntimeLocalStorageClaimToAssignment = async (
|
|
195
|
+
claimArg: unknown, assignmentArg: unknown, expectedClusterIdArg: unknown,
|
|
196
|
+
): Promise<{ claim: IRuntimeLocalStorageClaim; assignment: IRuntimeAssignment }> => {
|
|
197
|
+
const claim = snapshotRuntimeLocalStorageClaim(claimArg);
|
|
198
|
+
const assignment = safe(() => snapshotRuntimeAssignment(assignmentArg));
|
|
199
|
+
if ((await validateRuntimeLocalStorageClaim(claim)).length
|
|
200
|
+
|| (await validateRuntimeAssignment(assignment)).length) fail();
|
|
201
|
+
safe(() => {
|
|
202
|
+
const expectedClusterId = networkId(expectedClusterIdArg);
|
|
203
|
+
const reference = assignment.workload.storage.find((itemArg) => itemArg.id === claim.id);
|
|
204
|
+
requireNetwork(reference !== undefined
|
|
205
|
+
&& networkEqual(reference, getRuntimeLocalStorageClaimReference(claim))
|
|
206
|
+
&& claim.lifecycle === 'bound'
|
|
207
|
+
&& claim.clusterId === expectedClusterId
|
|
208
|
+
&& networkEqual(assignment.controller, claim.controller)
|
|
209
|
+
&& assignment.organizationId === claim.organizationId
|
|
210
|
+
&& assignment.nodeId === claim.nodeId
|
|
211
|
+
&& assignment.runtimeNamespace === claim.runtimeNamespace
|
|
212
|
+
&& assignment.serviceId === claim.serviceId);
|
|
213
|
+
});
|
|
214
|
+
return { claim, assignment };
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const snapshotApplyRuntimeLocalStorageClaimRequest = (
|
|
218
|
+
valueArg: unknown,
|
|
219
|
+
): IApplyRuntimeLocalStorageClaimRequest => safe(() => {
|
|
220
|
+
const value = networkRecord(captureNetwork(valueArg), ['session', 'claim']);
|
|
221
|
+
return { session: snapshotRuntimeSessionBinding(value.session), claim: snapshotRuntimeLocalStorageClaim(value.claim) };
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
export const snapshotApplyRuntimeLocalStorageClaimResponse = (
|
|
225
|
+
valueArg: unknown,
|
|
226
|
+
): IApplyRuntimeLocalStorageClaimResponse => safe(() => {
|
|
227
|
+
const value = networkRecord(captureNetwork(valueArg), ['status', 'state', 'claim']);
|
|
228
|
+
requireNetwork((responseStatuses as readonly unknown[]).includes(value.status)
|
|
229
|
+
&& (claimStates as readonly unknown[]).includes(value.state));
|
|
230
|
+
return {
|
|
231
|
+
status: value.status as IApplyRuntimeLocalStorageClaimResponse['status'],
|
|
232
|
+
state: value.state as TRuntimeLocalStorageClaimState,
|
|
233
|
+
claim: networkReference(value.claim),
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Admission-side content binding, not authentication: `trustedSessionArg` must be the node's
|
|
239
|
+
* CURRENT authenticated session, never read from the request. The request's session must equal it,
|
|
240
|
+
* the claim must name its controller, cluster, node and runtime namespace, and its digest must hold.
|
|
241
|
+
*/
|
|
242
|
+
export const bindApplyRuntimeLocalStorageClaimRequest = async (
|
|
243
|
+
valueArg: unknown, trustedSessionArg: unknown,
|
|
244
|
+
): Promise<IApplyRuntimeLocalStorageClaimRequest> => {
|
|
245
|
+
const request = snapshotApplyRuntimeLocalStorageClaimRequest(valueArg);
|
|
246
|
+
const trustedSession = safe(() => snapshotRuntimeSessionBinding(trustedSessionArg));
|
|
247
|
+
if ((await validateRuntimeLocalStorageClaim(request.claim)).length) fail();
|
|
248
|
+
safe(() => requireNetwork(networkEqual(request.session, trustedSession)
|
|
249
|
+
&& networkEqual(request.claim.controller, trustedSession.controller)
|
|
250
|
+
&& request.claim.clusterId === trustedSession.clusterId
|
|
251
|
+
&& request.claim.nodeId === trustedSession.nodeId
|
|
252
|
+
&& request.claim.runtimeNamespace === trustedSession.runtimeNamespace));
|
|
253
|
+
return request;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Matches an answer to the exact claim that was sent. An `accepted` or `replay` answer must state a
|
|
258
|
+
* state the claim can be in: a bound claim is `ready`, `import-required` (for an `import` claim
|
|
259
|
+
* only) or `uncertain`; a purge is `purged` or `uncertain`. A `historical` answer describes the
|
|
260
|
+
* newer generation the node holds, so its state is not judged against the sent claim.
|
|
261
|
+
*/
|
|
262
|
+
export const bindApplyRuntimeLocalStorageClaimResponse = (
|
|
263
|
+
valueArg: unknown, requestArg: unknown,
|
|
264
|
+
): IApplyRuntimeLocalStorageClaimResponse => safe(() => {
|
|
265
|
+
const response = snapshotApplyRuntimeLocalStorageClaimResponse(valueArg);
|
|
266
|
+
const { claim } = snapshotApplyRuntimeLocalStorageClaimRequest(requestArg);
|
|
267
|
+
requireNetwork(networkEqual(response.claim, getRuntimeLocalStorageClaimReference(claim)));
|
|
268
|
+
if (response.status !== 'historical') {
|
|
269
|
+
requireNetwork(claim.lifecycle === 'purge'
|
|
270
|
+
? response.state === 'purged' || response.state === 'uncertain'
|
|
271
|
+
: response.state === 'ready' || response.state === 'uncertain'
|
|
272
|
+
|| (response.state === 'import-required' && claim.initialization === 'import'));
|
|
273
|
+
}
|
|
274
|
+
return response;
|
|
275
|
+
});
|
|
@@ -76,19 +76,25 @@ export const serviceRuntimeContract = Object.freeze({
|
|
|
76
76
|
maximumAssignmentPageSize: 256,
|
|
77
77
|
} as const);
|
|
78
78
|
|
|
79
|
-
const serviceRuntimeWaitingReasons = [
|
|
79
|
+
const serviceRuntimeWaitingReasons = [
|
|
80
|
+
'node-network-pending', 'conflict', 'failure-backoff', 'storage-import-pending', 'bindings-pending',
|
|
81
|
+
] as const;
|
|
80
82
|
const serviceRuntimeRefusals = [
|
|
81
83
|
'cluster-runtime-missing', 'organization-missing', 'image-plan-missing', 'platform-unsupported',
|
|
82
84
|
'secrets-unsupported', 'volumes-unsupported', 'port-protocol-unsupported', 'ports-too-many',
|
|
83
85
|
'readiness-target-undeclared', 'environment-too-large', 'no-eligible-node', 'replica-limit',
|
|
84
86
|
'node-authority-unsupported', 'network-policy-missing', 'network-policy-mismatch',
|
|
85
87
|
'network-workload-pool-missing', 'network-workload-pool-exhausted',
|
|
88
|
+
'volume-driver-unsupported', 'volume-options-unsupported', 'volume-shared-unsupported',
|
|
89
|
+
'volume-placement-unsupported',
|
|
86
90
|
] as const;
|
|
87
91
|
|
|
88
92
|
/**
|
|
89
93
|
* Why the producer is not converging yet, though nothing needs an administrator:
|
|
90
94
|
* `node-network-pending` waits for a node's network readiness, `conflict` for a sweep over inputs
|
|
91
|
-
* that changed while it ran, `failure-backoff` for a failed replica's backoff to elapse
|
|
95
|
+
* that changed while it ran, `failure-backoff` for a failed replica's backoff to elapse,
|
|
96
|
+
* `storage-import-pending` for an `import` local storage claim whose bytes have not arrived on its
|
|
97
|
+
* node, and `bindings-pending` for the secret manifest to cover every enabled binding of the service.
|
|
92
98
|
*/
|
|
93
99
|
export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[number];
|
|
94
100
|
|
|
@@ -101,6 +107,11 @@ export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[n
|
|
|
101
107
|
* node whose session resolved fewer workload authorities than this spec needs, which the admit step
|
|
102
108
|
* refuses rather than handing the node work it would silently park. An attached network needs an
|
|
103
109
|
* explicit matching policy and a current workload address pool; neither is defaulted.
|
|
110
|
+
*
|
|
111
|
+
* The volume refusals name what a local storage claim cannot express: `volume-driver-unsupported`
|
|
112
|
+
* a volume of a driver other than a node-local directory, `volume-options-unsupported` driver or
|
|
113
|
+
* mount options, `volume-shared-unsupported` one volume mounted by more than one replica or service,
|
|
114
|
+
* and `volume-placement-unsupported` a placement that does not pin each volume to one node.
|
|
104
115
|
*/
|
|
105
116
|
export type TServiceRuntimeRefusal = typeof serviceRuntimeRefusals[number];
|
|
106
117
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical IP address and prefix text, read into numbers a prefix test can compare.
|
|
3
|
+
*
|
|
4
|
+
* Only the one canonical spelling of each value is accepted: a dotted quad without leading zeros
|
|
5
|
+
* for IPv4, and the RFC 5952 form for IPv6 (lowercase, no leading zeros, the longest run of two or
|
|
6
|
+
* more zero groups compressed, the first run on a tie). A value two peers could spell differently
|
|
7
|
+
* would let the same address pass one comparison and fail another, so a non-canonical spelling is
|
|
8
|
+
* refused rather than normalized. IPv6 text with an embedded dotted quad or a zone index is refused
|
|
9
|
+
* for the same reason.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type TIpAddressFamily = 4 | 6;
|
|
13
|
+
|
|
14
|
+
export interface IIpAddressValue {
|
|
15
|
+
family: TIpAddressFamily;
|
|
16
|
+
value: bigint;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IIpPrefixValue {
|
|
20
|
+
family: TIpAddressFamily;
|
|
21
|
+
network: bigint;
|
|
22
|
+
length: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const familyBits = (familyArg: TIpAddressFamily): number => (familyArg === 4 ? 32 : 128);
|
|
26
|
+
|
|
27
|
+
const parseIpv4 = (valueArg: string): bigint | null => {
|
|
28
|
+
if (valueArg.length > 15) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const octets = valueArg.split('.');
|
|
32
|
+
if (octets.length !== 4
|
|
33
|
+
|| !octets.every((octetArg) => /^(?:0|[1-9][0-9]{0,2})$/.test(octetArg) && Number(octetArg) <= 255)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return octets.reduce((sumArg, octetArg) => (sumArg << 8n) | BigInt(Number(octetArg)), 0n);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const parseIpv6Groups = (valueArg: string): string[] | null => {
|
|
40
|
+
if (valueArg.length > 39 || !/^[0-9a-f:]+$/.test(valueArg)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const halves = valueArg.split('::');
|
|
44
|
+
if (halves.length > 2) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const head = halves[0] === '' ? [] : halves[0].split(':');
|
|
48
|
+
if (halves.length === 1) {
|
|
49
|
+
return head;
|
|
50
|
+
}
|
|
51
|
+
const tail = halves[1] === '' ? [] : halves[1].split(':');
|
|
52
|
+
if (head.length + tail.length > 7) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return [...head, ...new Array<string>(8 - head.length - tail.length).fill('0'), ...tail];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const parseIpv6 = (valueArg: string): bigint | null => {
|
|
59
|
+
const groups = parseIpv6Groups(valueArg);
|
|
60
|
+
if (groups === null || groups.length !== 8
|
|
61
|
+
|| !groups.every((groupArg) => /^[0-9a-f]{1,4}$/.test(groupArg))) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return groups.reduce((sumArg, groupArg) => (sumArg << 16n) | BigInt(parseInt(groupArg, 16)), 0n);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** The RFC 5952 text of one IPv6 address. */
|
|
68
|
+
const formatIpv6 = (valueArg: bigint): string => {
|
|
69
|
+
const groups: number[] = [];
|
|
70
|
+
for (let index = 7; index >= 0; index--) {
|
|
71
|
+
groups.push(Number((valueArg >> BigInt(index * 16)) & 0xffffn));
|
|
72
|
+
}
|
|
73
|
+
let bestStart = -1;
|
|
74
|
+
let bestLength = 0;
|
|
75
|
+
for (let start = 0; start < 8;) {
|
|
76
|
+
if (groups[start] !== 0) {
|
|
77
|
+
start++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
let end = start;
|
|
81
|
+
while (end < 8 && groups[end] === 0) {
|
|
82
|
+
end++;
|
|
83
|
+
}
|
|
84
|
+
if (end - start > bestLength) {
|
|
85
|
+
bestStart = start;
|
|
86
|
+
bestLength = end - start;
|
|
87
|
+
}
|
|
88
|
+
start = end;
|
|
89
|
+
}
|
|
90
|
+
const text = groups.map((groupArg) => groupArg.toString(16));
|
|
91
|
+
if (bestLength < 2) {
|
|
92
|
+
return text.join(':');
|
|
93
|
+
}
|
|
94
|
+
return `${text.slice(0, bestStart).join(':')}::${text.slice(bestStart + bestLength).join(':')}`;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** One canonical address literal of either family, or `null` for anything else. */
|
|
98
|
+
export const parseCanonicalIpAddress = (valueArg: unknown): IIpAddressValue | null => {
|
|
99
|
+
if (typeof valueArg !== 'string') {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const ipv4 = parseIpv4(valueArg);
|
|
103
|
+
if (ipv4 !== null) {
|
|
104
|
+
return { family: 4, value: ipv4 };
|
|
105
|
+
}
|
|
106
|
+
const ipv6 = parseIpv6(valueArg);
|
|
107
|
+
return ipv6 !== null && formatIpv6(ipv6) === valueArg ? { family: 6, value: ipv6 } : null;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* One canonical prefix `<address>/<length>` of either family whose host bits are all zero, or
|
|
112
|
+
* `null` for anything else. The length is written without leading zeros.
|
|
113
|
+
*/
|
|
114
|
+
export const parseCanonicalIpPrefix = (valueArg: unknown): IIpPrefixValue | null => {
|
|
115
|
+
if (typeof valueArg !== 'string') {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const parts = valueArg.split('/');
|
|
119
|
+
if (parts.length !== 2 || !/^(?:0|[1-9][0-9]{0,2})$/.test(parts[1])) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const address = parseCanonicalIpAddress(parts[0]);
|
|
123
|
+
const length = Number(parts[1]);
|
|
124
|
+
if (address === null || length > familyBits(address.family)) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const hostBits = BigInt(familyBits(address.family) - length);
|
|
128
|
+
return address.value % (1n << hostBits) === 0n
|
|
129
|
+
? { family: address.family, network: address.value, length }
|
|
130
|
+
: null;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** True when the address lies inside the prefix; families never contain each other. */
|
|
134
|
+
export const ipPrefixContains = (prefixArg: IIpPrefixValue, addressArg: IIpAddressValue): boolean => {
|
|
135
|
+
if (prefixArg.family !== addressArg.family) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const hostBits = BigInt(familyBits(prefixArg.family) - prefixArg.length);
|
|
139
|
+
return addressArg.value >> hostBits === prefixArg.network >> hostBits;
|
|
140
|
+
};
|
|
@@ -614,6 +614,7 @@ export const createStorageContractReaders = (failArg: TCanonicalJsonFailure) =>
|
|
|
614
614
|
readCapacityQuantity,
|
|
615
615
|
capacityQuantityBytes,
|
|
616
616
|
readEnvironmentKey,
|
|
617
|
+
readMountPath,
|
|
617
618
|
normalizeCapacityRequest,
|
|
618
619
|
normalizeDelivery,
|
|
619
620
|
normalizeObjectStorageRequest,
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import * as clusterIngressInterfaces from '../data/clusteringress.js';
|
|
2
|
+
import type {
|
|
3
|
+
IClusterIngressDesignation,
|
|
4
|
+
IClusterIngressDesignationRequest,
|
|
5
|
+
} from '../data/clusteringressdesignation.js';
|
|
6
|
+
import type { IServiceRuntimeSpec } from '../data/serviceruntime.js';
|
|
7
|
+
import type { IIdentityCredential } from '../data/user.js';
|
|
2
8
|
import type { IProtocolOffer } from '../protocol.js';
|
|
3
9
|
import * as plugins from '../plugins.js';
|
|
4
10
|
|
|
@@ -70,6 +76,12 @@ export interface IReq_Relay_Cloudly_GetClusterRouteTable extends plugins.typedre
|
|
|
70
76
|
* Both hops judge the body in the one server order `protocol.readProtocolOffer`
|
|
71
77
|
* states, negotiating before the bearer is verified, as the shape states at
|
|
72
78
|
* `validateClusterIngressRegistration`.
|
|
79
|
+
*
|
|
80
|
+
* A protocol mismatch is refused with `IProtocolRefusal`; every other refusal
|
|
81
|
+
* travels as `IClusterIngressRegistrationRefusal` in `TypedResponseError.errorData`,
|
|
82
|
+
* naming the reason and the `retryAfterMs` the ingress waits before it registers
|
|
83
|
+
* again. The relay forwards Cloudly's refusal untouched and refuses with the same
|
|
84
|
+
* shape when it cannot reach Cloudly at all (`not-ready`).
|
|
73
85
|
*/
|
|
74
86
|
export interface IReq_Ingress_Relay_Cloudly_RegisterClusterIngress extends plugins.typedrequestInterfaces.implementsTR<
|
|
75
87
|
plugins.typedrequestInterfaces.ITypedRequest,
|
|
@@ -100,6 +112,25 @@ export interface IReq_Ingress_Relay_Cloudly_RegisterClusterIngress extends plugi
|
|
|
100
112
|
};
|
|
101
113
|
}
|
|
102
114
|
|
|
115
|
+
/**
|
|
116
|
+
* The relay tells its registered ingress that it holds a newer table.
|
|
117
|
+
*
|
|
118
|
+
* A hint, not a delivery: it carries the revision alone and nothing secret, and
|
|
119
|
+
* the ingress answers it by pulling `getClusterIngressRouteTable` with the
|
|
120
|
+
* revision it already applied. A lost or repeated hint costs nothing, because the
|
|
121
|
+
* table's own revision is the idempotency; a hint whose revision is not greater
|
|
122
|
+
* than the applied one is ignored.
|
|
123
|
+
*/
|
|
124
|
+
export interface IReq_Relay_Ingress_PushClusterIngressRouteTableChanged
|
|
125
|
+
extends plugins.typedrequestInterfaces.implementsTR<
|
|
126
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
127
|
+
IReq_Relay_Ingress_PushClusterIngressRouteTableChanged
|
|
128
|
+
> {
|
|
129
|
+
method: 'pushClusterIngressRouteTableChanged';
|
|
130
|
+
request: clusterIngressInterfaces.IClusterIngressRouteTableChange;
|
|
131
|
+
response: Record<string, never>;
|
|
132
|
+
}
|
|
133
|
+
|
|
103
134
|
/**
|
|
104
135
|
* The ingress fetches the table the relay holds.
|
|
105
136
|
*
|
|
@@ -147,3 +178,37 @@ export interface IReq_Any_Ingress_GetClusterTrafficStatistics extends plugins.ty
|
|
|
147
178
|
nextCursor?: string;
|
|
148
179
|
};
|
|
149
180
|
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Cloudly designates one existing service as the ingress of a cluster born on Pallet, creating the
|
|
184
|
+
* managed ingress bearer and the designation atomically. A cluster holds one kind of designation:
|
|
185
|
+
* Cloudly refuses a cluster that holds a fleet-cutover ingress designation.
|
|
186
|
+
*/
|
|
187
|
+
export interface IReq_Admin_Cloudly_DesignateClusterIngress
|
|
188
|
+
extends plugins.typedrequestInterfaces.implementsTR<
|
|
189
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
190
|
+
IReq_Admin_Cloudly_DesignateClusterIngress
|
|
191
|
+
> {
|
|
192
|
+
method: 'designateClusterIngress';
|
|
193
|
+
request: {
|
|
194
|
+
identity: IIdentityCredential;
|
|
195
|
+
designation: IClusterIngressDesignationRequest;
|
|
196
|
+
};
|
|
197
|
+
response: {
|
|
198
|
+
designation: IClusterIngressDesignation;
|
|
199
|
+
/** The ingress service's exact-cluster runtime spec the designation was accepted against. */
|
|
200
|
+
spec: IServiceRuntimeSpec;
|
|
201
|
+
replayed: boolean;
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The cluster's current ingress designation; `null` when the cluster holds none of this kind. */
|
|
206
|
+
export interface IReq_Admin_Cloudly_GetClusterIngressDesignation
|
|
207
|
+
extends plugins.typedrequestInterfaces.implementsTR<
|
|
208
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
209
|
+
IReq_Admin_Cloudly_GetClusterIngressDesignation
|
|
210
|
+
> {
|
|
211
|
+
method: 'getClusterIngressDesignation';
|
|
212
|
+
request: { identity: IIdentityCredential; clusterId: string };
|
|
213
|
+
response: { designation: IClusterIngressDesignation | null };
|
|
214
|
+
}
|
package/ts/requests/gateway.ts
CHANGED
|
@@ -5,6 +5,9 @@ import type {
|
|
|
5
5
|
IGatewayClientCredential,
|
|
6
6
|
IGatewayClientProvisioningSpec,
|
|
7
7
|
IGatewayClientContext,
|
|
8
|
+
IGatewayClientDnsRecord,
|
|
9
|
+
IGatewayClientDnsRecordSyncResult,
|
|
10
|
+
IGatewayClientHostnameOwnership,
|
|
8
11
|
IGatewayClientOwnership,
|
|
9
12
|
IGatewayClientRoute,
|
|
10
13
|
IGatewayDnsRecord,
|
|
@@ -13,6 +16,8 @@ import type {
|
|
|
13
16
|
IGatewayRouteConfig,
|
|
14
17
|
IGatewayRouteSyncResult,
|
|
15
18
|
TGatewayRouteDnsMode,
|
|
19
|
+
TGatewayClientCertificateReleaseResult,
|
|
20
|
+
TGatewayClientCertificateResult,
|
|
16
21
|
TGatewayClientProvisioningAction,
|
|
17
22
|
} from '../data/gateway.js';
|
|
18
23
|
import type { IMailOverviewDomain, IMailOverviewEmail } from './mail.js';
|
|
@@ -233,6 +238,72 @@ export interface IReq_SyncGatewayClientRoute extends plugins.typedrequestInterfa
|
|
|
233
238
|
response: IGatewayRouteSyncResult;
|
|
234
239
|
}
|
|
235
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Publishes, replaces or withdraws the one address record a gateway-client ownership holds.
|
|
243
|
+
*
|
|
244
|
+
* Gateway-client credentials only, with the `syncDnsRecords` capability; the hostname must match
|
|
245
|
+
* the client's `hostnamePatterns`. Idempotent: the same record again answers `managed-unchanged`,
|
|
246
|
+
* and deleting a record the ownership does not hold answers `managed-deleted`. The value must lie
|
|
247
|
+
* inside the client's `allowedDnsRecordAddresses` (`target-invalid` otherwise). A hostname held by
|
|
248
|
+
* anything else is `record-conflict` and stays untouched. Requires `dns.clientRecords`.
|
|
249
|
+
*/
|
|
250
|
+
export interface IReq_SyncGatewayClientDnsRecord extends plugins.typedrequestInterfaces.implementsTR<
|
|
251
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
252
|
+
IReq_SyncGatewayClientDnsRecord
|
|
253
|
+
> {
|
|
254
|
+
method: 'syncGatewayClientDnsRecord';
|
|
255
|
+
request: {
|
|
256
|
+
apiToken: string;
|
|
257
|
+
identity?: never;
|
|
258
|
+
ownership: IGatewayClientHostnameOwnership;
|
|
259
|
+
} & (
|
|
260
|
+
| { record: IGatewayClientDnsRecord; delete?: never }
|
|
261
|
+
| { delete: true; record?: never }
|
|
262
|
+
);
|
|
263
|
+
response: IGatewayClientDnsRecordSyncResult;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Answers the exact certificate for one owned hostname, starting its issuance when none is valid.
|
|
268
|
+
*
|
|
269
|
+
* Gateway-client credentials only, with `requestCertificates` and `readCertificates`; the hostname
|
|
270
|
+
* must match the client's `hostnamePatterns`. No gateway route is needed. The certificate is issued
|
|
271
|
+
* for exactly this one identifier in the client's own namespace, and asking again after
|
|
272
|
+
* `renewAfter` answers a renewed one. The `ready` answer carries the private key. Requires
|
|
273
|
+
* `certificates.exactIssuance`.
|
|
274
|
+
*/
|
|
275
|
+
export interface IReq_GetGatewayClientCertificate extends plugins.typedrequestInterfaces.implementsTR<
|
|
276
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
277
|
+
IReq_GetGatewayClientCertificate
|
|
278
|
+
> {
|
|
279
|
+
method: 'getGatewayClientCertificate';
|
|
280
|
+
request: {
|
|
281
|
+
apiToken: string;
|
|
282
|
+
identity?: never;
|
|
283
|
+
ownership: IGatewayClientHostnameOwnership;
|
|
284
|
+
};
|
|
285
|
+
response: TGatewayClientCertificateResult;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Removes the certificate material and any issuance an ownership holds, releasing its certificate claim; an address record under the same ownership is unaffected.
|
|
290
|
+
*
|
|
291
|
+
* Gateway-client credentials only, with `requestCertificates`. Idempotent: an ownership that holds
|
|
292
|
+
* nothing answers `unchanged`. Requires `certificates.exactIssuance`.
|
|
293
|
+
*/
|
|
294
|
+
export interface IReq_ReleaseGatewayClientCertificate extends plugins.typedrequestInterfaces.implementsTR<
|
|
295
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
296
|
+
IReq_ReleaseGatewayClientCertificate
|
|
297
|
+
> {
|
|
298
|
+
method: 'releaseGatewayClientCertificate';
|
|
299
|
+
request: {
|
|
300
|
+
apiToken: string;
|
|
301
|
+
identity?: never;
|
|
302
|
+
ownership: IGatewayClientHostnameOwnership;
|
|
303
|
+
};
|
|
304
|
+
response: TGatewayClientCertificateReleaseResult;
|
|
305
|
+
}
|
|
306
|
+
|
|
236
307
|
export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestInterfaces.implementsTR<
|
|
237
308
|
plugins.typedrequestInterfaces.ITypedRequest,
|
|
238
309
|
IReq_ReprovisionCertificateDomain
|
|
@@ -8,6 +8,7 @@ import type * as credential from '../data/networkcredential.js';
|
|
|
8
8
|
import type * as assignmentSecret from '../runtime.assignmentsecret.js';
|
|
9
9
|
import type * as fleet from '../data/fleetcutover.js';
|
|
10
10
|
import type { IRuntimeStorageClaim } from '../data/runtimestorageclaim.js';
|
|
11
|
+
import type * as localStorage from '../data/runtimelocalstorageclaim.js';
|
|
11
12
|
|
|
12
13
|
/** Current-peer, exact-projection authorization; response contains secret keys. */
|
|
13
14
|
export interface IReq_Pallet_Controller_GetRuntimeManagedVpnCredential extends plugins.typedrequestInterfaces.implementsTR<
|
|
@@ -163,6 +164,21 @@ export interface IReq_Controller_Pallet_ApplyRuntimeAssignment extends plugins.t
|
|
|
163
164
|
response: data.IApplyRuntimeAssignmentResponse;
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Controller push of one local storage claim over the already authenticated Pallet-initiated
|
|
169
|
+
* connection; `bindApplyRuntimeLocalStorageClaimRequest` binds it to that session. The answer is a
|
|
170
|
+
* durable admission acknowledgement plus the volume's state; it grants no mount by itself, which
|
|
171
|
+
* only an assignment that seals the claim's reference does.
|
|
172
|
+
*/
|
|
173
|
+
export interface IReq_Controller_Pallet_ApplyRuntimeLocalStorageClaim extends plugins.typedrequestInterfaces.implementsTR<
|
|
174
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
175
|
+
IReq_Controller_Pallet_ApplyRuntimeLocalStorageClaim
|
|
176
|
+
> {
|
|
177
|
+
method: 'applyRuntimeLocalStorageClaim';
|
|
178
|
+
request: localStorage.IApplyRuntimeLocalStorageClaimRequest;
|
|
179
|
+
response: localStorage.IApplyRuntimeLocalStorageClaimResponse;
|
|
180
|
+
}
|
|
181
|
+
|
|
166
182
|
/** Current-peer report, including exact historical outbox recovery after reconnect. */
|
|
167
183
|
export interface IReq_Pallet_Controller_ReportRuntimeAssignmentObservation extends plugins.typedrequestInterfaces.implementsTR<
|
|
168
184
|
plugins.typedrequestInterfaces.ITypedRequest,
|