@serve.zone/interfaces 32.29.0 → 32.31.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 +76 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/mail.d.ts +3 -3
- package/dist_ts/data/networkauthority.d.ts +14 -0
- package/dist_ts/data/networkauthority.js +12 -1
- package/dist_ts/data/networkprojection.d.ts +148 -13
- package/dist_ts/data/networkprojection.js +91 -22
- package/dist_ts/data/serviceruntime.d.ts +27 -3
- package/dist_ts/data/serviceruntime.js +3 -2
- package/dist_ts/platform/corestoreendpointselection.d.ts +37 -0
- package/dist_ts/platform/corestoreendpointselection.js +45 -0
- package/dist_ts/platform/index.d.ts +3 -1
- package/dist_ts/platform/index.js +4 -2
- package/dist_ts/platform/objectstorage.js +5 -1
- package/dist_ts/private/networkcontract.d.ts +2 -0
- package/dist_ts/private/networkcontract.js +9 -4
- package/dist_ts/requests/network.d.ts +60 -2
- package/package.json +3 -3
- package/readme.md +93 -10
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/mail.ts +3 -3
- package/ts/data/networkauthority.ts +12 -0
- package/ts/data/networkprojection.ts +220 -34
- package/ts/data/serviceruntime.ts +28 -3
- package/ts/platform/corestoreendpointselection.ts +79 -0
- package/ts/platform/index.ts +3 -0
- package/ts/platform/objectstorage.ts +4 -0
- package/ts/private/networkcontract.ts +7 -3
- package/ts/requests/network.ts +56 -2
|
@@ -2,7 +2,7 @@ import type { TSha256Digest } from './immutableimage.js';
|
|
|
2
2
|
import type { IRuntimeAssignmentReference, IRuntimeAuthorityReference, IRuntimeController } from './runtimeassignment.js';
|
|
3
3
|
import { isRuntimeReplicaId, snapshotRuntimeAssignment, validateRuntimeAssignment } from './runtimeassignment.js';
|
|
4
4
|
import type { IRuntimeNetworkHandoffLease, IRuntimeNetworkProtectedAuthority } from './networkauthority.js';
|
|
5
|
-
import { bindRuntimeNetworkHandoffLease, computeRuntimeNetworkProtectedAuthorityDigest,
|
|
5
|
+
import { bindRuntimeNetworkHandoffLease, computeRuntimeNetworkProtectedAuthorityDigest, runtimeNetworkAuthorityContract,
|
|
6
6
|
snapshotRuntimeNetworkHandoffLease, snapshotRuntimeNetworkProtectedAuthority } from './networkauthority.js';
|
|
7
7
|
import type { IPrivateNetwork, IServicePrivateNetworks, IServicePrivateNetworkDnsPolicy } from './privatenetwork.js';
|
|
8
8
|
import { routerPacketGrantsOf, snapshotRouterProjection,
|
|
@@ -61,15 +61,35 @@ export interface IRuntimeNetworkWorkloadLease extends IRuntimeNetworkNodeScope {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* One node-level published port for a workload.
|
|
65
|
-
*
|
|
64
|
+
* One node-level published port, or one contiguous range of them, for a workload.
|
|
65
|
+
*
|
|
66
|
+
* Without `hostPortEnd` the entry publishes exactly `hostPort` to `targetPort`. With it the entry
|
|
67
|
+
* publishes every port from `hostPort` to `hostPortEnd` inclusive, each to the same port inside the
|
|
68
|
+
* workload, so `targetPort` equals `hostPort`. Every port an entry covers is claimed on its node:
|
|
69
|
+
* two entries of one node and protocol never share a port, and the entries of one endpoint never
|
|
70
|
+
* overlap and cover at most `runtimeNetworkPublishedPortContract.maximumPortsPerService` ports.
|
|
66
71
|
*/
|
|
67
72
|
export interface IRuntimeNetworkPublishedPort {
|
|
68
73
|
protocol: 'tcp' | 'udp';
|
|
69
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* 1..65535; the first port of a range. Cloudly publishes only ports inside
|
|
76
|
+
* `runtimeNetworkPublishedPortContract.publishablePorts` (`published-port-reserved`).
|
|
77
|
+
*/
|
|
70
78
|
hostPort: number;
|
|
71
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* The last port of a range, above `hostPort` and at most 65535. Omitted, the entry is one port;
|
|
81
|
+
* a one-port range is never written with it.
|
|
82
|
+
*/
|
|
83
|
+
hostPortEnd?: number;
|
|
84
|
+
/** The workload's own listening port inside its network namespace; `hostPort` for a range. */
|
|
72
85
|
targetPort: number;
|
|
86
|
+
/**
|
|
87
|
+
* Outbound flows the workload originates from a published target port leave the node from the
|
|
88
|
+
* matching host port, so a peer sees one address and port in both directions (SIP, RTP). Only
|
|
89
|
+
* `true` is ever stated; omitted, outbound flows keep the handoff lease's translation. It grants
|
|
90
|
+
* no egress: an endpoint carrying a symmetric entry has `publicEgress: true`.
|
|
91
|
+
*/
|
|
92
|
+
symmetric?: true;
|
|
73
93
|
/**
|
|
74
94
|
* Never the wildcard or loopback. Omitted means the node's uplink address.
|
|
75
95
|
* The contract cannot prove an uplink fact, so binding the port to that exact
|
|
@@ -84,37 +104,121 @@ export interface IRuntimeNetworkPublishedPort {
|
|
|
84
104
|
}
|
|
85
105
|
|
|
86
106
|
/**
|
|
87
|
-
* Caller-authored published-port request. It carries no `hostIp` (an omitted
|
|
88
|
-
*
|
|
89
|
-
* The `authorization` reference of the resulting projection entry is derived by
|
|
90
|
-
*
|
|
107
|
+
* Caller-authored published-port request. It carries no `hostIp` (an omitted address means the
|
|
108
|
+
* node uplink). `hostPortEnd` and `symmetric` follow the rules of `IRuntimeNetworkPublishedPort`.
|
|
109
|
+
* The `authorization` reference of the resulting projection entry is derived by Cloudly from the
|
|
110
|
+
* policy revision and digest, never supplied by the caller.
|
|
91
111
|
*/
|
|
92
112
|
export interface IRuntimeNetworkPublishedPortRequest {
|
|
93
113
|
protocol: 'tcp' | 'udp';
|
|
94
114
|
hostPort: number;
|
|
115
|
+
/** The last port of a range; `targetPort` then equals `hostPort`. */
|
|
116
|
+
hostPortEnd?: number;
|
|
95
117
|
targetPort: number;
|
|
118
|
+
/** Same-port outbound; valid only while the service's policy document grants public egress. */
|
|
119
|
+
symmetric?: true;
|
|
96
120
|
}
|
|
97
121
|
|
|
98
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Current authorized published-port policy for one service.
|
|
124
|
+
*
|
|
125
|
+
* The published-port half of the service's one policy document; `IServiceNetworkEgress` reads the
|
|
126
|
+
* egress half of the same document, so both carry the same `revision`.
|
|
127
|
+
*/
|
|
99
128
|
export interface IServicePublishedPorts {
|
|
100
129
|
organizationId: string;
|
|
101
130
|
serviceId: string;
|
|
102
131
|
revision: number;
|
|
103
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Sorted by protocol and host port, never overlapping, at most
|
|
134
|
+
* `runtimeNetworkPublishedPortContract.maximumEntries` entries covering at most its
|
|
135
|
+
* `maximumPortsPerService` ports. Empty means the service publishes nothing.
|
|
136
|
+
*/
|
|
104
137
|
publishedPorts: IRuntimeNetworkPublishedPortRequest[];
|
|
105
138
|
}
|
|
106
139
|
|
|
107
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* Exact desired policy. Node-level host-port placement remains Cloudly's decision.
|
|
142
|
+
*
|
|
143
|
+
* Beyond the shape its snapshot checks, Cloudly refuses a port outside
|
|
144
|
+
* `runtimeNetworkPublishedPortContract.publishablePorts` and a symmetric entry while the document's
|
|
145
|
+
* `publicEgress` is false; `setServiceNetworkEgress` likewise refuses clearing `publicEgress` while a
|
|
146
|
+
* symmetric entry remains.
|
|
147
|
+
*
|
|
148
|
+
* One document per service holds its published ports and its egress under one `revision`. The
|
|
149
|
+
* first accepted write of either (`expectedRevision: null`) creates it: `setServicePublishedPorts`
|
|
150
|
+
* with egress `publicEgress: false` and `platformEndpointIds: []`, `setServiceNetworkEgress` with
|
|
151
|
+
* no published ports. From then on both reads return it, never null. It is an explicit policy: an
|
|
152
|
+
* attached spec stating `publicEgress: false` and `platformEndpointIds: []` matches it and reaches
|
|
153
|
+
* no public address and no platform endpoint; `network-policy-missing` means the service has no
|
|
154
|
+
* document at all.
|
|
155
|
+
*/
|
|
108
156
|
export interface ISetServicePublishedPorts {
|
|
109
157
|
organizationId: string;
|
|
110
158
|
serviceId: string;
|
|
111
159
|
/** Retain the exact body and this identifier across retries. */
|
|
112
160
|
mutationId: string;
|
|
113
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* The revision of the service's one policy document the caller read, shared with its egress: an
|
|
163
|
+
* accepted `ISetServiceNetworkEgress` advances it too. null means the service has never had a
|
|
164
|
+
* policy document, neither published ports nor egress; it is not an empty list.
|
|
165
|
+
*/
|
|
114
166
|
expectedRevision: number | null;
|
|
115
167
|
publishedPorts: IRuntimeNetworkPublishedPortRequest[];
|
|
116
168
|
}
|
|
117
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Current authorized egress policy for one service: whether its attached workloads reach the public
|
|
172
|
+
* internet, and which platform endpoints of the protected authority they reach.
|
|
173
|
+
*
|
|
174
|
+
* The egress half of the service's one policy document; `IServicePublishedPorts` reads the
|
|
175
|
+
* published-port half of the same document, so both carry the same `revision`. Every projection
|
|
176
|
+
* endpoint of the service carries exactly these values (`IRuntimeNetworkEndpoint`), and an attached
|
|
177
|
+
* runtime spec (`TServiceRuntimeNetwork`) is admitted only while it states the same ones: its
|
|
178
|
+
* disagreement is `network-policy-mismatch`, a service without a document `network-policy-missing`.
|
|
179
|
+
* Nothing else grants a workload egress.
|
|
180
|
+
*/
|
|
181
|
+
export interface IServiceNetworkEgress {
|
|
182
|
+
organizationId: string;
|
|
183
|
+
serviceId: string;
|
|
184
|
+
revision: number;
|
|
185
|
+
/** TCP/UDP only; the complete protected union remains denied. */
|
|
186
|
+
publicEgress: boolean;
|
|
187
|
+
/**
|
|
188
|
+
* Ids of `IRuntimeNetworkProtectedAuthority.platformEndpoints`, sorted and unique, at most
|
|
189
|
+
* `runtimeNetworkAuthorityContract.maximumPlatformEndpoints`. Empty selects none.
|
|
190
|
+
*/
|
|
191
|
+
platformEndpointIds: string[];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Exact desired egress. The published ports of the same document are carried forward unchanged.
|
|
196
|
+
* Whether each id names a current platform endpoint is Cloudly's decision at acceptance, and so is
|
|
197
|
+
* the refusal of `publicEgress: false` while a carried published port is `symmetric`.
|
|
198
|
+
*
|
|
199
|
+
* One document per service holds its published ports and its egress under one `revision`. The
|
|
200
|
+
* first accepted write of either (`expectedRevision: null`) creates it: `setServicePublishedPorts`
|
|
201
|
+
* with egress `publicEgress: false` and `platformEndpointIds: []`, `setServiceNetworkEgress` with
|
|
202
|
+
* no published ports. From then on both reads return it, never null. It is an explicit policy: an
|
|
203
|
+
* attached spec stating `publicEgress: false` and `platformEndpointIds: []` matches it and reaches
|
|
204
|
+
* no public address and no platform endpoint; `network-policy-missing` means the service has no
|
|
205
|
+
* document at all.
|
|
206
|
+
*/
|
|
207
|
+
export interface ISetServiceNetworkEgress {
|
|
208
|
+
organizationId: string;
|
|
209
|
+
serviceId: string;
|
|
210
|
+
/** Retain the exact body and this identifier across retries. */
|
|
211
|
+
mutationId: string;
|
|
212
|
+
/**
|
|
213
|
+
* The revision of the service's one policy document the caller read, shared with its published
|
|
214
|
+
* ports: an accepted `ISetServicePublishedPorts` advances it too. null means the service has
|
|
215
|
+
* never had a policy document, neither published ports nor egress.
|
|
216
|
+
*/
|
|
217
|
+
expectedRevision: number | null;
|
|
218
|
+
publicEgress: boolean;
|
|
219
|
+
platformEndpointIds: string[];
|
|
220
|
+
}
|
|
221
|
+
|
|
118
222
|
/** Controller-owned readiness evidence. A signature does not prove this evidence's provenance. */
|
|
119
223
|
export interface IRuntimeNetworkEndpoint {
|
|
120
224
|
lease: IRuntimeNetworkWorkloadLease;
|
|
@@ -128,7 +232,11 @@ export interface IRuntimeNetworkEndpoint {
|
|
|
128
232
|
/** TCP/UDP only; the complete protected union remains denied. */
|
|
129
233
|
publicEgress: boolean;
|
|
130
234
|
platformEndpointIds: string[];
|
|
131
|
-
/**
|
|
235
|
+
/**
|
|
236
|
+
* Sorted by protocol and host port and never overlapping, within the bounds of
|
|
237
|
+
* `runtimeNetworkPublishedPortContract`. Absent publishes nothing; a present member is never
|
|
238
|
+
* empty. A `symmetric` entry is admitted only while `publicEgress` is true.
|
|
239
|
+
*/
|
|
132
240
|
publishedPorts?: IRuntimeNetworkPublishedPort[];
|
|
133
241
|
}
|
|
134
242
|
|
|
@@ -216,6 +324,23 @@ export const runtimeNetworkProjectionContract = Object.freeze({
|
|
|
216
324
|
allocationReuse: 'requires-controller-quarantine-and-all-affected-authority-fences',
|
|
217
325
|
} as const);
|
|
218
326
|
|
|
327
|
+
/**
|
|
328
|
+
* The bounds of one service's published ports, the same on its policy document and on each of its
|
|
329
|
+
* projection endpoints.
|
|
330
|
+
*
|
|
331
|
+
* `maximumEntries` bounds the list and `maximumPortsPerService` the ports its entries cover
|
|
332
|
+
* together, a range counting every port in it; the snapshots refuse either excess.
|
|
333
|
+
* `publishablePorts` is Cloudly's placement rule, not a snapshot rule: the ports above it are
|
|
334
|
+
* `runtimeNetworkHandoffSnatPortRange`, which a handoff lease translates outbound flows to, so
|
|
335
|
+
* Cloudly refuses a policy with a port outside it as `published-port-reserved`
|
|
336
|
+
* (`isRuntimeNetworkPublishedPortReserved`). A policy written before this rule stays readable.
|
|
337
|
+
*/
|
|
338
|
+
export const runtimeNetworkPublishedPortContract = Object.freeze({
|
|
339
|
+
maximumEntries: 64,
|
|
340
|
+
maximumPortsPerService: 1024,
|
|
341
|
+
publishablePorts: Object.freeze({ first: 1, last: 49_151 } as const),
|
|
342
|
+
} as const);
|
|
343
|
+
|
|
219
344
|
/** The assignment contract owns the replica rule; a lease names the same slot by the same name. */
|
|
220
345
|
const replicaId = (value: unknown): string => {
|
|
221
346
|
requireNetwork(isRuntimeReplicaId(value)); return value as string;
|
|
@@ -276,28 +401,59 @@ const packetGrant = (value: unknown): IRuntimeNetworkPacketGrant => {
|
|
|
276
401
|
return { source: networkReference(item.source), destination: { kind, id: networkId(destination.id), address: networkIpv4(destination.address),
|
|
277
402
|
protocol: destination.protocol, port: networkCounter(destination.port, 65535) } };
|
|
278
403
|
};
|
|
279
|
-
|
|
404
|
+
type TPublishedPortSpan = Pick<IRuntimeNetworkPublishedPortRequest, 'protocol' | 'hostPort' | 'hostPortEnd'>;
|
|
405
|
+
const publishedPortKey = (value: TPublishedPortSpan): string =>
|
|
280
406
|
`${value.protocol}/${String(value.hostPort).padStart(5, '0')}`;
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
407
|
+
const publishedPortLast = (value: TPublishedPortSpan): number => value.hostPortEnd ?? value.hostPort;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Whether any port an entry covers lies outside `runtimeNetworkPublishedPortContract.publishablePorts`:
|
|
411
|
+
* Cloudly's `published-port-reserved`. It judges an entry its snapshot already accepted.
|
|
412
|
+
*/
|
|
413
|
+
export const isRuntimeNetworkPublishedPortReserved = (value: TPublishedPortSpan): boolean =>
|
|
414
|
+
value.hostPort < runtimeNetworkPublishedPortContract.publishablePorts.first
|
|
415
|
+
|| publishedPortLast(value) > runtimeNetworkPublishedPortContract.publishablePorts.last;
|
|
416
|
+
|
|
417
|
+
/** The members every published-port entry shares; an optional member is stated only when present. */
|
|
418
|
+
const publishedPortSpan = (item: Record<string, unknown>): IRuntimeNetworkPublishedPortRequest => {
|
|
284
419
|
requireNetwork(item.protocol === 'tcp' || item.protocol === 'udp');
|
|
285
|
-
const
|
|
420
|
+
const hostPort = networkCounter(item.hostPort, 65535), targetPort = networkCounter(item.targetPort, 65535);
|
|
421
|
+
const hasEnd = Object.hasOwn(item, 'hostPortEnd'), hasSymmetric = Object.hasOwn(item, 'symmetric');
|
|
422
|
+
const hostPortEnd = hasEnd ? networkCounter(item.hostPortEnd, 65535) : hostPort;
|
|
423
|
+
// A range maps each host port to the same workload port; a one-port range has one encoding, none.
|
|
424
|
+
requireNetwork(!hasEnd || (hostPortEnd > hostPort && targetPort === hostPort));
|
|
425
|
+
requireNetwork(!hasSymmetric || item.symmetric === true);
|
|
426
|
+
return { protocol: item.protocol, hostPort, ...(hasEnd ? { hostPortEnd } : {}), targetPort,
|
|
427
|
+
...(hasSymmetric ? { symmetric: true as const } : {}) };
|
|
428
|
+
};
|
|
429
|
+
/** Sorted, never overlapping within a protocol, and bounded in entries and in covered ports. */
|
|
430
|
+
const publishedPortList = <T extends TPublishedPortSpan>(value: unknown, read: (item: unknown) => T): T[] => {
|
|
431
|
+
const ports = networkSorted(networkList(value, runtimeNetworkPublishedPortContract.maximumEntries, read), publishedPortKey);
|
|
432
|
+
let covered = 0;
|
|
433
|
+
ports.forEach((port, index) => {
|
|
434
|
+
const previous = index === 0 ? null : ports[index - 1];
|
|
435
|
+
requireNetwork(previous === null || previous.protocol !== port.protocol || publishedPortLast(previous) < port.hostPort);
|
|
436
|
+
covered += publishedPortLast(port) - port.hostPort + 1;
|
|
437
|
+
});
|
|
438
|
+
requireNetwork(covered <= runtimeNetworkPublishedPortContract.maximumPortsPerService);
|
|
439
|
+
return ports;
|
|
440
|
+
};
|
|
441
|
+
const publishedPort = (value: unknown): IRuntimeNetworkPublishedPort => {
|
|
442
|
+
const item = networkRecord(value, ['protocol', 'hostPort', 'targetPort', 'authorization'], ['hostIp', 'hostPortEnd', 'symmetric']);
|
|
443
|
+
const { symmetric, ...span } = publishedPortSpan(item);
|
|
444
|
+
const hostIp = Object.hasOwn(item, 'hostIp') ? networkIpv4(item.hostIp) : null;
|
|
286
445
|
requireNetwork(hostIp === null || (hostIp !== '0.0.0.0' && !networkContains('127.0.0.0/8', `${hostIp}/32`)));
|
|
287
|
-
return {
|
|
288
|
-
|
|
446
|
+
return { ...span, ...(hostIp === null ? {} : { hostIp }), ...(symmetric ? { symmetric } : {}),
|
|
447
|
+
authorization: networkReference(item.authorization) };
|
|
289
448
|
};
|
|
290
449
|
const publishedPorts = (value: unknown): IRuntimeNetworkPublishedPort[] => {
|
|
291
|
-
const ports =
|
|
450
|
+
const ports = publishedPortList(value, publishedPort);
|
|
292
451
|
requireNetwork(ports.length > 0); return ports;
|
|
293
452
|
};
|
|
294
|
-
const publishedPortRequest = (value: unknown): IRuntimeNetworkPublishedPortRequest =>
|
|
295
|
-
|
|
296
|
-
requireNetwork(item.protocol === 'tcp' || item.protocol === 'udp');
|
|
297
|
-
return { protocol: item.protocol, hostPort: networkCounter(item.hostPort, 65535), targetPort: networkCounter(item.targetPort, 65535) };
|
|
298
|
-
};
|
|
453
|
+
const publishedPortRequest = (value: unknown): IRuntimeNetworkPublishedPortRequest =>
|
|
454
|
+
publishedPortSpan(networkRecord(value, ['protocol', 'hostPort', 'targetPort'], ['hostPortEnd', 'symmetric']));
|
|
299
455
|
const publishedPortRequests = (value: unknown): IRuntimeNetworkPublishedPortRequest[] =>
|
|
300
|
-
|
|
456
|
+
publishedPortList(value, publishedPortRequest);
|
|
301
457
|
|
|
302
458
|
/** Structural snapshot only; node placement, quota and authorization remain Cloudly's decision. */
|
|
303
459
|
export const snapshotServicePublishedPorts = (value: unknown): IServicePublishedPorts => networkSafe(() => {
|
|
@@ -313,6 +469,28 @@ export const snapshotSetServicePublishedPorts = (value: unknown): ISetServicePub
|
|
|
313
469
|
publishedPorts: publishedPortRequests(item.publishedPorts) };
|
|
314
470
|
});
|
|
315
471
|
|
|
472
|
+
const servicePlatformEndpointIds = (value: unknown): string[] => networkSorted(
|
|
473
|
+
networkList(value, runtimeNetworkAuthorityContract.maximumPlatformEndpoints, networkId), id => id);
|
|
474
|
+
const servicePublicEgress = (value: unknown): boolean => {
|
|
475
|
+
requireNetwork(typeof value === 'boolean'); return value;
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
/** Structural snapshot only; whether each id names a current platform endpoint remains Cloudly's decision. */
|
|
479
|
+
export const snapshotServiceNetworkEgress = (value: unknown): IServiceNetworkEgress => networkSafe(() => {
|
|
480
|
+
const item = networkRecord(value, ['organizationId', 'serviceId', 'revision', 'publicEgress', 'platformEndpointIds']);
|
|
481
|
+
return { organizationId: networkId(item.organizationId), serviceId: networkId(item.serviceId),
|
|
482
|
+
revision: networkCounter(item.revision), publicEgress: servicePublicEgress(item.publicEgress),
|
|
483
|
+
platformEndpointIds: servicePlatformEndpointIds(item.platformEndpointIds) };
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
export const snapshotSetServiceNetworkEgress = (value: unknown): ISetServiceNetworkEgress => networkSafe(() => {
|
|
487
|
+
const item = networkRecord(value, ['organizationId', 'serviceId', 'mutationId', 'expectedRevision', 'publicEgress',
|
|
488
|
+
'platformEndpointIds']);
|
|
489
|
+
return { organizationId: networkId(item.organizationId), serviceId: networkId(item.serviceId),
|
|
490
|
+
mutationId: networkId(item.mutationId), expectedRevision: item.expectedRevision === null ? null : networkCounter(item.expectedRevision),
|
|
491
|
+
publicEgress: servicePublicEgress(item.publicEgress), platformEndpointIds: servicePlatformEndpointIds(item.platformEndpointIds) };
|
|
492
|
+
});
|
|
493
|
+
|
|
316
494
|
const endpoint = (value: unknown): IRuntimeNetworkEndpoint => {
|
|
317
495
|
const hasPublishedPorts = value !== null && typeof value === 'object' && Object.hasOwn(value, 'publishedPorts');
|
|
318
496
|
const item = networkRecord(value, ['lease', 'ready', 'observation', 'publicEgress', 'platformEndpointIds',
|
|
@@ -328,9 +506,12 @@ const endpoint = (value: unknown): IRuntimeNetworkEndpoint => {
|
|
|
328
506
|
reporterSessionId: networkId(observed.reporterSessionId), sequence: networkCounter(observed.sequence), digest: networkDigest(observed.digest) };
|
|
329
507
|
}
|
|
330
508
|
requireNetwork(!item.ready || (observation !== null && observation.assignment.generation === 1));
|
|
509
|
+
const ports = hasPublishedPorts ? publishedPorts(item.publishedPorts) : null;
|
|
510
|
+
// Same-port outbound rides the public egress grant; it never grants egress of its own.
|
|
511
|
+
requireNetwork(item.publicEgress || !(ports ?? []).some(port => port.symmetric));
|
|
331
512
|
return { lease, ready: item.ready, observation, publicEgress: item.publicEgress,
|
|
332
513
|
platformEndpointIds: networkSorted(networkList(item.platformEndpointIds, 64, networkId), value => value),
|
|
333
|
-
...(
|
|
514
|
+
...(ports === null ? {} : { publishedPorts: ports }) };
|
|
334
515
|
};
|
|
335
516
|
|
|
336
517
|
/** Structural snapshot only; use validate/signature/admission helpers before accepting authority. */
|
|
@@ -435,7 +616,8 @@ export const validateRuntimeNetworkProjection = async (value: unknown): Promise<
|
|
|
435
616
|
requireNetwork(!aliases.has(name)); aliases.add(name);
|
|
436
617
|
}
|
|
437
618
|
}
|
|
438
|
-
const assignments = new Set<string>(), attempts = new Set<string>(), readyReplicas = new Set<string>()
|
|
619
|
+
const assignments = new Set<string>(), attempts = new Set<string>(), readyReplicas = new Set<string>();
|
|
620
|
+
const hostPortClaims: Array<{ node: string; first: number; last: number }> = [];
|
|
439
621
|
for (const endpoint of projection.endpoints) {
|
|
440
622
|
const lease = endpoint.lease, authority = allocationAuthority(lease.protectedAuthority);
|
|
441
623
|
// A controller on the node itself has no other node: every endpoint it projects is local.
|
|
@@ -454,12 +636,16 @@ export const validateRuntimeNetworkProjection = async (value: unknown): Promise<
|
|
|
454
636
|
pool.id === lease.ipv4.poolId && pool.purpose === 'workload' && networkContains(pool.prefix, lease.ipv4.linkSubnet)));
|
|
455
637
|
if (networkSameScope(lease, projection)) requireNetwork(projection.handoff !== null && lease.routerIncarnationId === projection.handoff.routerIncarnationId);
|
|
456
638
|
for (const id of endpoint.platformEndpointIds) requireNetwork(current.platformEndpoints.some(platform => platform.id === id));
|
|
457
|
-
for (const port of endpoint.publishedPorts ?? []) {
|
|
458
|
-
|
|
459
|
-
requireNetwork(!hostPorts.has(claim)); hostPorts.add(claim);
|
|
460
|
-
}
|
|
639
|
+
for (const port of endpoint.publishedPorts ?? []) hostPortClaims.push({ node: networkCanonical([lease.nodeId, port.protocol]),
|
|
640
|
+
first: port.hostPort, last: publishedPortLast(port) });
|
|
461
641
|
requireNetwork(!projection.retiredLeases.some(retired => networkEqual(retired, networkReferenceOf(lease))));
|
|
462
642
|
}
|
|
643
|
+
// Every port of every entry is claimed on its node: ordered by start, no claim reaches the next one.
|
|
644
|
+
hostPortClaims.sort((left, right) => left.node < right.node ? -1 : left.node > right.node ? 1 : left.first - right.first);
|
|
645
|
+
for (let index = 1; index < hostPortClaims.length; index++) {
|
|
646
|
+
const previous = hostPortClaims[index - 1], current = hostPortClaims[index];
|
|
647
|
+
requireNetwork(previous.node !== current.node || previous.last < current.first);
|
|
648
|
+
}
|
|
463
649
|
for (const id of projection.hostPlatformEndpointIds ?? []) requireNetwork(current.platformEndpoints.some(platform => platform.id === id));
|
|
464
650
|
for (let index = 0; index < projection.endpoints.length; index++) for (let previous = 0; previous < index; previous++) {
|
|
465
651
|
requireNetwork(!networkOverlaps(projection.endpoints[index].lease.ipv4.linkSubnet, projection.endpoints[previous].lease.ipv4.linkSubnet));
|
|
@@ -33,7 +33,8 @@ export interface IServiceRuntimeProfile {
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* `isolated` gives the workload no network authority. `attached` joins the runtime network with
|
|
36
|
-
* public egress on or off and the platform endpoints it may reach, by id: sorted and unique.
|
|
36
|
+
* public egress on or off and the platform endpoints it may reach, by id: sorted and unique. The
|
|
37
|
+
* service's `IServiceNetworkEgress` must state the same values, or the workload is not admitted.
|
|
37
38
|
*/
|
|
38
39
|
export type TServiceRuntimeNetwork =
|
|
39
40
|
| { mode: 'isolated' }
|
|
@@ -89,7 +90,8 @@ const serviceRuntimeRefusals = [
|
|
|
89
90
|
'volume-driver-unsupported', 'volume-options-unsupported', 'volume-shared-unsupported',
|
|
90
91
|
'volume-placement-unsupported', 'volume-initialization-unknown', 'volume-ownership-unknown',
|
|
91
92
|
'objectstorage-endpoint-missing', 'objectstorage-binding-invalid', 'objectstorage-environment-conflict',
|
|
92
|
-
'database-endpoint-missing',
|
|
93
|
+
'objectstorage-endpoint-unselected', 'database-endpoint-missing', 'database-endpoint-unselected',
|
|
94
|
+
'published-port-reserved', 'published-port-symmetric-without-egress',
|
|
93
95
|
] as const;
|
|
94
96
|
|
|
95
97
|
/**
|
|
@@ -114,7 +116,8 @@ export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[n
|
|
|
114
116
|
* `port-protocol-unsupported` is about one port's protocol — and `node-authority-unsupported` is a
|
|
115
117
|
* node whose session resolved fewer workload authorities than this spec needs, which the admit step
|
|
116
118
|
* refuses rather than handing the node work it would silently park. An attached network needs an
|
|
117
|
-
* explicit matching policy
|
|
119
|
+
* explicit matching policy (`IServiceNetworkEgress`, written by `setServiceNetworkEgress`) and a
|
|
120
|
+
* current workload address pool; neither is defaulted.
|
|
118
121
|
*
|
|
119
122
|
* The volume refusals name what a local storage claim cannot express: `volume-driver-unsupported`
|
|
120
123
|
* a volume of a driver other than a node-local directory, `volume-options-unsupported` driver or
|
|
@@ -133,6 +136,28 @@ export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[n
|
|
|
133
136
|
* `database-endpoint-missing` is an enabled database binding placed on a node that states no valid
|
|
134
137
|
* `corestoreDatabase`: its credential cannot be addressed to any host a workload reaches, so it is
|
|
135
138
|
* neither granted nor published, and the workload is not started with a guessed one.
|
|
139
|
+
*
|
|
140
|
+
* The two `-unselected` refusals name a binding endpoint the workload's network cannot reach, so a
|
|
141
|
+
* workload is refused rather than started against a connection that stays dark. A workload reaches
|
|
142
|
+
* a platform endpoint only when its spec's attached network selects it (`platformEndpointIds`), and
|
|
143
|
+
* an `isolated` network selects none. `database-endpoint-unselected` is an enabled database binding
|
|
144
|
+
* whose node's valid `corestoreDatabase` is not a selected endpoint: none of the selected
|
|
145
|
+
* `IRuntimeNetworkProtectedAuthority.platformEndpoints` has the endpoint's `host` as its `address`,
|
|
146
|
+
* `tcp` as its `protocol` and the endpoint's `port` as its `port`. `objectstorage-endpoint-unselected`
|
|
147
|
+
* is the same for an enabled object-storage binding's node's valid `corestoreObjectStorage`, whose
|
|
148
|
+
* `endpoint` origin states the host and the port (the scheme's default, 80 for `http:` and 443 for
|
|
149
|
+
* `https:`, when it states none). Platform endpoints are IPv4, so an endpoint whose host is a DNS name
|
|
150
|
+
* or an IPv6 literal is never selected. Each is judged only for an endpoint that is valid: a node
|
|
151
|
+
* stating none, or an invalid one, stays `database-endpoint-missing` or `objectstorage-endpoint-missing`.
|
|
152
|
+
* `platform.findUnselectedCorestoreEndpoints` is this rule; a producer answers what it returns.
|
|
153
|
+
*
|
|
154
|
+
* The two `published-port-` refusals name a service policy document whose published ports no
|
|
155
|
+
* projection may carry, so its endpoints are not compiled rather than published on some ports only.
|
|
156
|
+
* `published-port-reserved` is an entry covering a port outside
|
|
157
|
+
* `runtimeNetworkPublishedPortContract.publishablePorts` (`isRuntimeNetworkPublishedPortReserved`),
|
|
158
|
+
* which the handoff leases' `runtimeNetworkHandoffSnatPortRange` owns; a document written before
|
|
159
|
+
* that rule can hold one. `published-port-symmetric-without-egress` is a `symmetric` entry in a
|
|
160
|
+
* document whose `publicEgress` is false, which a projection endpoint refuses.
|
|
136
161
|
*/
|
|
137
162
|
export type TServiceRuntimeRefusal = typeof serviceRuntimeRefusals[number];
|
|
138
163
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { IRuntimeNetworkProtectedAuthority } from '../data/networkauthority.js';
|
|
2
|
+
import type { TServiceRuntimeNetwork, TServiceRuntimeRefusal } from '../data/serviceruntime.js';
|
|
3
|
+
import { isNetworkIpv4 } from '../private/networkcontract.js';
|
|
4
|
+
import { validatePlatformDatabaseEndpoint, type IPlatformDatabaseEndpoint } from './database.js';
|
|
5
|
+
import { validatePlatformObjectStorageEndpoint, type IPlatformObjectStorageEndpoint } from './objectstorage.js';
|
|
6
|
+
|
|
7
|
+
/** The service runtime refusals a Corestore endpoint the workload's network does not select raises. */
|
|
8
|
+
export type TCorestoreEndpointSelectionRefusal = Extract<TServiceRuntimeRefusal,
|
|
9
|
+
'database-endpoint-unselected' | 'objectstorage-endpoint-unselected'>;
|
|
10
|
+
|
|
11
|
+
/** What `findUnselectedCorestoreEndpoints` judges for one node a service's bindings are placed on. */
|
|
12
|
+
export interface ICorestoreEndpointSelection {
|
|
13
|
+
/**
|
|
14
|
+
* The node's `corestoreDatabase` when an enabled database binding of the service is placed on it,
|
|
15
|
+
* otherwise `null`. A node stating none is `database-endpoint-missing`, never judged here.
|
|
16
|
+
*/
|
|
17
|
+
database: IPlatformDatabaseEndpoint | null;
|
|
18
|
+
/**
|
|
19
|
+
* The node's `corestoreObjectStorage` when an enabled object-storage binding of the service is
|
|
20
|
+
* placed on it, otherwise `null`. A node stating none is `objectstorage-endpoint-missing`.
|
|
21
|
+
*/
|
|
22
|
+
objectStorage: IPlatformObjectStorageEndpoint | null;
|
|
23
|
+
/** The current protected authority's platform endpoints, which the selected ids resolve against. */
|
|
24
|
+
platformEndpoints: IRuntimeNetworkProtectedAuthority['platformEndpoints'];
|
|
25
|
+
/** The runtime spec's network: `attached` selects its `platformEndpointIds`, `isolated` selects none. */
|
|
26
|
+
network: TServiceRuntimeNetwork;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type TPlatformEndpoint = IRuntimeNetworkProtectedAuthority['platformEndpoints'][number];
|
|
30
|
+
|
|
31
|
+
const selectedPlatformEndpoints = (selectionArg: ICorestoreEndpointSelection): TPlatformEndpoint[] => {
|
|
32
|
+
if (selectionArg.network.mode !== 'attached') return [];
|
|
33
|
+
const selectedIds = new Set(selectionArg.network.platformEndpointIds);
|
|
34
|
+
return selectionArg.platformEndpoints.filter((endpointArg) => selectedIds.has(endpointArg.id));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Platform endpoints are IPv4, so only a canonical IPv4 host can be one; a DNS name or IPv6 literal never is. */
|
|
38
|
+
const selectsTcp = (endpointsArg: TPlatformEndpoint[], hostArg: string, portArg: number): boolean => (
|
|
39
|
+
isNetworkIpv4(hostArg) && endpointsArg.some((endpointArg) => endpointArg.address === hostArg
|
|
40
|
+
&& endpointArg.protocol === 'tcp' && endpointArg.port === portArg)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/** The origin's port, or the scheme's default (80 for `http:`, 443 for `https:`) when it states none. */
|
|
44
|
+
const objectStorageOriginPort = (originArg: URL): number => (
|
|
45
|
+
originArg.port === '' ? (originArg.protocol === 'https:' ? 443 : 80) : Number(originArg.port)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Which of a node's Corestore endpoints a service's workload would dial without its network
|
|
50
|
+
* selecting them, as the service runtime refusals that name them; empty when none.
|
|
51
|
+
*
|
|
52
|
+
* An endpoint is selected when one of the spec's selected platform endpoints has the endpoint's host
|
|
53
|
+
* as its `address`, `tcp` as its `protocol` and the endpoint's port as its `port`: the database
|
|
54
|
+
* endpoint's `host` and `port`, and the object-storage endpoint origin's host and port, or 80 for
|
|
55
|
+
* `http:` and 443 for `https:` when the origin states none. An `isolated` network selects nothing, a
|
|
56
|
+
* selected id the protected authority does not hold selects nothing, and a DNS-name or IPv6 host is
|
|
57
|
+
* never selected. Only a valid endpoint is judged: one `validatePlatformDatabaseEndpoint` or
|
|
58
|
+
* `validatePlatformObjectStorageEndpoint` refuses is the `-missing` refusal's, not this one's. The
|
|
59
|
+
* answer is sorted, so it does not depend on the order of the inputs.
|
|
60
|
+
*/
|
|
61
|
+
export const findUnselectedCorestoreEndpoints = (
|
|
62
|
+
selectionArg: ICorestoreEndpointSelection,
|
|
63
|
+
): TCorestoreEndpointSelectionRefusal[] => {
|
|
64
|
+
const selected = selectedPlatformEndpoints(selectionArg);
|
|
65
|
+
const refusals: TCorestoreEndpointSelectionRefusal[] = [];
|
|
66
|
+
const database = selectionArg.database;
|
|
67
|
+
if (database !== null && validatePlatformDatabaseEndpoint(database).length === 0
|
|
68
|
+
&& !selectsTcp(selected, database.host, database.port)) {
|
|
69
|
+
refusals.push('database-endpoint-unselected');
|
|
70
|
+
}
|
|
71
|
+
const objectStorage = selectionArg.objectStorage;
|
|
72
|
+
if (objectStorage !== null && validatePlatformObjectStorageEndpoint(objectStorage).length === 0) {
|
|
73
|
+
const origin = new URL(objectStorage.endpoint);
|
|
74
|
+
if (!selectsTcp(selected, origin.hostname, objectStorageOriginPort(origin))) {
|
|
75
|
+
refusals.push('objectstorage-endpoint-unselected');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return refusals;
|
|
79
|
+
};
|
package/ts/platform/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as ai from './ai.js';
|
|
2
2
|
import * as backup from './backup.js';
|
|
3
|
+
import * as corestoreendpointselection from './corestoreendpointselection.js';
|
|
3
4
|
import * as database from './database.js';
|
|
4
5
|
import * as email from './email.js';
|
|
5
6
|
import * as letter from './letter.js';
|
|
@@ -17,6 +18,7 @@ import * as types from './types.js';
|
|
|
17
18
|
export {
|
|
18
19
|
ai,
|
|
19
20
|
backup,
|
|
21
|
+
corestoreendpointselection,
|
|
20
22
|
database,
|
|
21
23
|
email,
|
|
22
24
|
letter,
|
|
@@ -32,6 +34,7 @@ export {
|
|
|
32
34
|
types,
|
|
33
35
|
};
|
|
34
36
|
|
|
37
|
+
export * from './corestoreendpointselection.js';
|
|
35
38
|
export * from './database.js';
|
|
36
39
|
export * from './storage.js';
|
|
37
40
|
export * from './objectstorage.js';
|
|
@@ -102,6 +102,10 @@ export const validatePlatformObjectStorageEndpoint = (
|
|
|
102
102
|
errors.push(`${pathArg} endpoint must be a canonical origin without a path, query, `
|
|
103
103
|
+ 'fragment, trailing slash or default port');
|
|
104
104
|
}
|
|
105
|
+
// A URL parser refuses a port above 65535 but reads port 0 as stated; no workload dials it.
|
|
106
|
+
if (parsedEndpoint.port !== '' && Number(parsedEndpoint.port) < 1) {
|
|
107
|
+
errors.push(`${pathArg} endpoint port must be from 1 to 65535`);
|
|
108
|
+
}
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
if (typeof valueArg.region !== 'string'
|
|
@@ -209,10 +209,14 @@ export const networkHash = async (domain: string, value: { digest: TSha256Digest
|
|
|
209
209
|
const { digest: omitted, ...payload } = value;
|
|
210
210
|
return `sha256:${await createCanonicalJsonSha256Hex(networkCanonical({ domain, payload }), failNetwork)}`;
|
|
211
211
|
};
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
/** A canonical dotted-quad IPv4 literal: four decimal octets, no leading zeros. */
|
|
213
|
+
export const isNetworkIpv4 = (value: unknown): value is string => {
|
|
214
|
+
if (typeof value !== 'string' || value.length > 15) return false;
|
|
214
215
|
const parts = value.split('.');
|
|
215
|
-
|
|
216
|
+
return parts.length === 4 && parts.every(part => /^(?:0|[1-9][0-9]{0,2})$/.test(part) && Number(part) <= 255);
|
|
217
|
+
};
|
|
218
|
+
export const networkIpv4 = (value: unknown): string => {
|
|
219
|
+
requireNetwork(isNetworkIpv4(value));
|
|
216
220
|
return value;
|
|
217
221
|
};
|
|
218
222
|
export const networkIpv4Number = (value: string): number => value.split('.').reduce((sum, part) => sum * 256 + Number(part), 0);
|
package/ts/requests/network.ts
CHANGED
|
@@ -6,7 +6,9 @@ import type {
|
|
|
6
6
|
IPrivateNetwork, IServicePrivateNetworks, ISetServicePrivateNetworks,
|
|
7
7
|
TPrivateNetworkMutation,
|
|
8
8
|
} from '../data/privatenetwork.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
IServiceNetworkEgress, IServicePublishedPorts, ISetServiceNetworkEgress, ISetServicePublishedPorts,
|
|
11
|
+
} from '../data/networkprojection.js';
|
|
10
12
|
|
|
11
13
|
/** Current organization picker data. Entries are never cached authorization grants. */
|
|
12
14
|
export interface IReq_Cloudly_ListOwnedOrganizations extends plugins.typedrequestInterfaces.implementsTR<
|
|
@@ -71,7 +73,7 @@ export interface IReq_Cloudly_SetServicePrivateNetworks extends plugins.typedreq
|
|
|
71
73
|
response: { membership: IServicePrivateNetworks; replayed: boolean };
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
/** null means the service has never had
|
|
76
|
+
/** null means the service has never had a policy document, neither published ports nor egress. */
|
|
75
77
|
export interface IReq_Cloudly_GetServicePublishedPorts extends plugins.typedrequestInterfaces.implementsTR<
|
|
76
78
|
plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_GetServicePublishedPorts
|
|
77
79
|
> {
|
|
@@ -80,6 +82,17 @@ export interface IReq_Cloudly_GetServicePublishedPorts extends plugins.typedrequ
|
|
|
80
82
|
response: { publishedPorts: IServicePublishedPorts | null };
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Writes the published-port half of one service's policy document, carrying its egress forward.
|
|
87
|
+
*
|
|
88
|
+
* One document per service holds its published ports and its egress under one `revision`. The
|
|
89
|
+
* first accepted write of either (`expectedRevision: null`) creates it: `setServicePublishedPorts`
|
|
90
|
+
* with egress `publicEgress: false` and `platformEndpointIds: []`, `setServiceNetworkEgress` with
|
|
91
|
+
* no published ports. From then on both reads return it, never null. It is an explicit policy: an
|
|
92
|
+
* attached spec stating `publicEgress: false` and `platformEndpointIds: []` matches it and reaches
|
|
93
|
+
* no public address and no platform endpoint; `network-policy-missing` means the service has no
|
|
94
|
+
* document at all.
|
|
95
|
+
*/
|
|
83
96
|
export interface IReq_Cloudly_SetServicePublishedPorts extends plugins.typedrequestInterfaces.implementsTR<
|
|
84
97
|
plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_SetServicePublishedPorts
|
|
85
98
|
> {
|
|
@@ -93,6 +106,47 @@ export interface IReq_Cloudly_SetServicePublishedPorts extends plugins.typedrequ
|
|
|
93
106
|
response: { publishedPorts: IServicePublishedPorts; replayed: boolean };
|
|
94
107
|
}
|
|
95
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The egress half of one service's policy document; null means the service has never had a policy
|
|
111
|
+
* document. Only a verified administrator reads it: platform endpoints are platform authority.
|
|
112
|
+
*/
|
|
113
|
+
export interface IReq_Admin_Cloudly_GetServiceNetworkEgress extends plugins.typedrequestInterfaces.implementsTR<
|
|
114
|
+
plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_GetServiceNetworkEgress
|
|
115
|
+
> {
|
|
116
|
+
method: 'getServiceNetworkEgress';
|
|
117
|
+
request: { identity: IIdentityCredential; organizationId: string; serviceId: string };
|
|
118
|
+
response: { networkEgress: IServiceNetworkEgress | null };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Writes the egress half of one service's policy document, carrying its published ports forward.
|
|
123
|
+
*
|
|
124
|
+
* Only a verified administrator writes it, as only an administrator writes the runtime spec whose
|
|
125
|
+
* attached network must state the same values. `expectedRevision` is the revision of the one
|
|
126
|
+
* document shared with the published ports; any other stored revision refuses the write, and an id
|
|
127
|
+
* that names no current platform endpoint refuses it. A retry of the exact body under the same
|
|
128
|
+
* `mutationId` answers the stored document with `replayed: true`.
|
|
129
|
+
*
|
|
130
|
+
* One document per service holds its published ports and its egress under one `revision`. The
|
|
131
|
+
* first accepted write of either (`expectedRevision: null`) creates it: `setServicePublishedPorts`
|
|
132
|
+
* with egress `publicEgress: false` and `platformEndpointIds: []`, `setServiceNetworkEgress` with
|
|
133
|
+
* no published ports. From then on both reads return it, never null. It is an explicit policy: an
|
|
134
|
+
* attached spec stating `publicEgress: false` and `platformEndpointIds: []` matches it and reaches
|
|
135
|
+
* no public address and no platform endpoint; `network-policy-missing` means the service has no
|
|
136
|
+
* document at all.
|
|
137
|
+
*/
|
|
138
|
+
export interface IReq_Admin_Cloudly_SetServiceNetworkEgress extends plugins.typedrequestInterfaces.implementsTR<
|
|
139
|
+
plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_SetServiceNetworkEgress
|
|
140
|
+
> {
|
|
141
|
+
method: 'setServiceNetworkEgress';
|
|
142
|
+
request: { identity: IIdentityCredential; mutation: ISetServiceNetworkEgress };
|
|
143
|
+
/**
|
|
144
|
+
* The accepted document including its `revision`; historical on replay. Acceptance is neither an
|
|
145
|
+
* admitted workload nor applied packet state.
|
|
146
|
+
*/
|
|
147
|
+
response: { networkEgress: IServiceNetworkEgress; replayed: boolean };
|
|
148
|
+
}
|
|
149
|
+
|
|
96
150
|
export interface IRequest_Any_Cloudly_GetNetworkNodes {
|
|
97
151
|
method: 'getNetworkNodes';
|
|
98
152
|
request: {};
|