@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
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import * as clusterIngressInterfaces from '../data/clusteringress.js';
|
|
2
2
|
import * as plugins from '../plugins.js';
|
|
3
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
3
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2x1c3RlcmluZ3Jlc3MuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9yZXF1ZXN0cy9jbHVzdGVyaW5ncmVzcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssd0JBQXdCLE1BQU0sMkJBQTJCLENBQUM7QUFRdEUsT0FBTyxLQUFLLE9BQU8sTUFBTSxlQUFlLENBQUMifQ==
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
-
import type { IGatewayCapabilities, IGatewayClient, IGatewayClientCredential, IGatewayClientProvisioningSpec, IGatewayClientContext, IGatewayClientOwnership, IGatewayClientRoute, IGatewayDnsRecord, IGatewayDomain, IGatewayIdentity, IGatewayRouteConfig, IGatewayRouteSyncResult, TGatewayRouteDnsMode, TGatewayClientProvisioningAction } from '../data/gateway.js';
|
|
2
|
+
import type { IGatewayCapabilities, IGatewayClient, IGatewayClientCredential, IGatewayClientProvisioningSpec, IGatewayClientContext, IGatewayClientDnsRecord, IGatewayClientDnsRecordSyncResult, IGatewayClientHostnameOwnership, IGatewayClientOwnership, IGatewayClientRoute, IGatewayDnsRecord, IGatewayDomain, IGatewayIdentity, IGatewayRouteConfig, IGatewayRouteSyncResult, TGatewayRouteDnsMode, TGatewayClientCertificateReleaseResult, TGatewayClientCertificateResult, TGatewayClientProvisioningAction } from '../data/gateway.js';
|
|
3
3
|
import type { IMailOverviewDomain, IMailOverviewEmail } from './mail.js';
|
|
4
4
|
/**
|
|
5
5
|
* Gateway client contracts: the machine-facing API a gateway (dcrouter)
|
|
@@ -172,6 +172,63 @@ export interface IReq_SyncGatewayClientRoute extends plugins.typedrequestInterfa
|
|
|
172
172
|
};
|
|
173
173
|
response: IGatewayRouteSyncResult;
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Publishes, replaces or withdraws the one address record a gateway-client ownership holds.
|
|
177
|
+
*
|
|
178
|
+
* Gateway-client credentials only, with the `syncDnsRecords` capability; the hostname must match
|
|
179
|
+
* the client's `hostnamePatterns`. Idempotent: the same record again answers `managed-unchanged`,
|
|
180
|
+
* and deleting a record the ownership does not hold answers `managed-deleted`. The value must lie
|
|
181
|
+
* inside the client's `allowedDnsRecordAddresses` (`target-invalid` otherwise). A hostname held by
|
|
182
|
+
* anything else is `record-conflict` and stays untouched. Requires `dns.clientRecords`.
|
|
183
|
+
*/
|
|
184
|
+
export interface IReq_SyncGatewayClientDnsRecord extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_SyncGatewayClientDnsRecord> {
|
|
185
|
+
method: 'syncGatewayClientDnsRecord';
|
|
186
|
+
request: {
|
|
187
|
+
apiToken: string;
|
|
188
|
+
identity?: never;
|
|
189
|
+
ownership: IGatewayClientHostnameOwnership;
|
|
190
|
+
} & ({
|
|
191
|
+
record: IGatewayClientDnsRecord;
|
|
192
|
+
delete?: never;
|
|
193
|
+
} | {
|
|
194
|
+
delete: true;
|
|
195
|
+
record?: never;
|
|
196
|
+
});
|
|
197
|
+
response: IGatewayClientDnsRecordSyncResult;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Answers the exact certificate for one owned hostname, starting its issuance when none is valid.
|
|
201
|
+
*
|
|
202
|
+
* Gateway-client credentials only, with `requestCertificates` and `readCertificates`; the hostname
|
|
203
|
+
* must match the client's `hostnamePatterns`. No gateway route is needed. The certificate is issued
|
|
204
|
+
* for exactly this one identifier in the client's own namespace, and asking again after
|
|
205
|
+
* `renewAfter` answers a renewed one. The `ready` answer carries the private key. Requires
|
|
206
|
+
* `certificates.exactIssuance`.
|
|
207
|
+
*/
|
|
208
|
+
export interface IReq_GetGatewayClientCertificate extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_GetGatewayClientCertificate> {
|
|
209
|
+
method: 'getGatewayClientCertificate';
|
|
210
|
+
request: {
|
|
211
|
+
apiToken: string;
|
|
212
|
+
identity?: never;
|
|
213
|
+
ownership: IGatewayClientHostnameOwnership;
|
|
214
|
+
};
|
|
215
|
+
response: TGatewayClientCertificateResult;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Removes the certificate material and any issuance an ownership holds, releasing its certificate claim; an address record under the same ownership is unaffected.
|
|
219
|
+
*
|
|
220
|
+
* Gateway-client credentials only, with `requestCertificates`. Idempotent: an ownership that holds
|
|
221
|
+
* nothing answers `unchanged`. Requires `certificates.exactIssuance`.
|
|
222
|
+
*/
|
|
223
|
+
export interface IReq_ReleaseGatewayClientCertificate extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_ReleaseGatewayClientCertificate> {
|
|
224
|
+
method: 'releaseGatewayClientCertificate';
|
|
225
|
+
request: {
|
|
226
|
+
apiToken: string;
|
|
227
|
+
identity?: never;
|
|
228
|
+
ownership: IGatewayClientHostnameOwnership;
|
|
229
|
+
};
|
|
230
|
+
response: TGatewayClientCertificateReleaseResult;
|
|
231
|
+
}
|
|
175
232
|
export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_ReprovisionCertificateDomain> {
|
|
176
233
|
method: 'reprovisionCertificateDomain';
|
|
177
234
|
request: {
|
|
@@ -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
|
/** Current-peer, exact-projection authorization; response contains secret keys. */
|
|
12
13
|
export interface IReq_Pallet_Controller_GetRuntimeManagedVpnCredential extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Pallet_Controller_GetRuntimeManagedVpnCredential> {
|
|
13
14
|
method: 'getRuntimeManagedVpnCredential';
|
|
@@ -125,6 +126,17 @@ export interface IReq_Controller_Pallet_ApplyRuntimeAssignment extends plugins.t
|
|
|
125
126
|
request: data.IApplyRuntimeAssignmentRequest;
|
|
126
127
|
response: data.IApplyRuntimeAssignmentResponse;
|
|
127
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Controller push of one local storage claim over the already authenticated Pallet-initiated
|
|
131
|
+
* connection; `bindApplyRuntimeLocalStorageClaimRequest` binds it to that session. The answer is a
|
|
132
|
+
* durable admission acknowledgement plus the volume's state; it grants no mount by itself, which
|
|
133
|
+
* only an assignment that seals the claim's reference does.
|
|
134
|
+
*/
|
|
135
|
+
export interface IReq_Controller_Pallet_ApplyRuntimeLocalStorageClaim extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Controller_Pallet_ApplyRuntimeLocalStorageClaim> {
|
|
136
|
+
method: 'applyRuntimeLocalStorageClaim';
|
|
137
|
+
request: localStorage.IApplyRuntimeLocalStorageClaimRequest;
|
|
138
|
+
response: localStorage.IApplyRuntimeLocalStorageClaimResponse;
|
|
139
|
+
}
|
|
128
140
|
/** Current-peer report, including exact historical outbox recovery after reconnect. */
|
|
129
141
|
export interface IReq_Pallet_Controller_ReportRuntimeAssignmentObservation extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Pallet_Controller_ReportRuntimeAssignmentObservation> {
|
|
130
142
|
method: 'reportRuntimeAssignmentObservation';
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -572,6 +572,41 @@ overlapping current grants reject, and up to 4,096 retained withdrawals carry
|
|
|
572
572
|
forward until the caller supplies its exact joined application of the preceding
|
|
573
573
|
projection.
|
|
574
574
|
|
|
575
|
+
The optional `IRuntimeNetworkProjection.workloadIngress` member exists only on
|
|
576
|
+
`cloudly` projections; any other controller kind carrying it rejects. It is how
|
|
577
|
+
the cluster ingress workload reaches the workloads it routes to without a
|
|
578
|
+
published host port: each `grants` entry names the exact lease reference of the
|
|
579
|
+
ingress workload (`source`), the exact lease reference of the target workload
|
|
580
|
+
(`destination`), a protocol, the target's own listening port and an
|
|
581
|
+
`authorization` reference. Unlike the full-mesh workload grants, it needs neither
|
|
582
|
+
the same organization nor a shared private network, it opens exactly one port,
|
|
583
|
+
and it is one-way: nothing flows back except the replies of a connection the
|
|
584
|
+
ingress opened. Both leases must be endpoints of the projection, they must
|
|
585
|
+
differ, and at least one of them must be placed on the projection's own node.
|
|
586
|
+
Entries are unique and strictly ordered by `[source.id, destination.id, protocol,
|
|
587
|
+
port]`; at most 1,024 are allowed (`runtimeNetworkWorkloadIngressContract`). The
|
|
588
|
+
authorization is signed content, not grant identity. A projection that omits the
|
|
589
|
+
member retains its exact canonical bytes, digest and signature.
|
|
590
|
+
`getRuntimeNetworkProjectionWorkloadIngressPacketGrants` validates the complete
|
|
591
|
+
projection and returns detached `IRuntimeNetworkWorkloadIngressPacketGrant`s
|
|
592
|
+
(`{ source, destination: { lease, protocol, port } }`). Their withdrawals follow
|
|
593
|
+
the host rules: an omitted grant needs its exact withdrawal, unknown or current
|
|
594
|
+
denials reject, and up to 4,096 retained withdrawals carry forward until the
|
|
595
|
+
exact joined application of the preceding projection is supplied. Cloudly routes
|
|
596
|
+
a cluster ingress route to the target lease address and port once the grant
|
|
597
|
+
exists.
|
|
598
|
+
|
|
599
|
+
The optional `IRuntimeNetworkProjection.hostPlatformEndpointIds` lists the ids of
|
|
600
|
+
the protected authority's platform endpoints whose address the projection's
|
|
601
|
+
node's own host carries, such as a relay listener or a Corestore beside the node
|
|
602
|
+
on one machine. A node delivers workload flows to those endpoints locally instead
|
|
603
|
+
of forwarding them. The list is sorted, unique, never empty when present, and
|
|
604
|
+
every id must be a current platform endpoint; omitting it keeps the canonical
|
|
605
|
+
bytes and states that no endpoint is local. It grants nothing: a workload still
|
|
606
|
+
reaches only the endpoints it selects. The platform endpoint contract itself
|
|
607
|
+
(`{ id, address, protocol, port }`) names no node, which is why this statement is
|
|
608
|
+
per projection.
|
|
609
|
+
|
|
575
610
|
`admitRuntimeNetworkProjection` checks exact predecessor/replay identity and
|
|
576
611
|
rejects changed same-generation content, regressed membership/readiness evidence,
|
|
577
612
|
changed immutable address/DNS keys, and omitted packet grants without explicit
|
|
@@ -811,6 +846,15 @@ named once; an empty `hubs` composes the authority of a runtime network whose cl
|
|
|
811
846
|
yet. One hub costs one of the authority's 64 platform endpoints, so 64 clusters fit one
|
|
812
847
|
authority.
|
|
813
848
|
|
|
849
|
+
A single host that carries both a cluster's hub and platform endpoints, for example the relay
|
|
850
|
+
listener and a Corestore, holds more than one address. The hub address sits inside `vpn.hubPrefix`,
|
|
851
|
+
as every hub's does. The plan's own platform endpoints may not: they sit on another address of the
|
|
852
|
+
same host, inside `prefixes`, outside every pool and outside both VPN prefixes. Several endpoints may
|
|
853
|
+
share that one address on distinct ports, because the authority requires only each
|
|
854
|
+
`address:port:protocol` to be unique. For example, with `hubPrefix` `192.0.2.0/28`, the hub binds
|
|
855
|
+
`192.0.2.1` and the plan lists the relay listener and Corestore on `192.0.2.17` with two ports. The
|
|
856
|
+
node's projection then names those endpoints in `hostPlatformEndpointIds`.
|
|
857
|
+
|
|
814
858
|
`admitRuntimeNetworkAddressPlanChange(previous | null, next)` is the pure decision for a plan write at
|
|
815
859
|
the following revision; any other revision is a contract violation. Leases and the managed VPN
|
|
816
860
|
outlive a plan revision, so it answers:
|
|
@@ -1373,12 +1417,18 @@ phase, settlement, release or authority booleans or evidence.
|
|
|
1373
1417
|
`data.IServiceRuntimeStatus { id, specRevision, state, reason, changedAt }` is the producer's verdict
|
|
1374
1418
|
on one spec revision, and each reason belongs to exactly one state. `converged` and `converging`
|
|
1375
1419
|
carry `reason: null`. `waiting` carries a `TServiceRuntimeWaitingReason`: `node-network-pending`,
|
|
1376
|
-
`conflict
|
|
1420
|
+
`conflict`, `failure-backoff`, `storage-import-pending` (an `import` local storage claim whose bytes
|
|
1421
|
+
have not arrived on its node) or `bindings-pending` (the secret manifest does not cover every enabled
|
|
1422
|
+
binding of the service yet). `refused` carries a `TServiceRuntimeRefusal`:
|
|
1377
1423
|
`cluster-runtime-missing`, `organization-missing`, `image-plan-missing`, `platform-unsupported`,
|
|
1378
1424
|
`secrets-unsupported`, `volumes-unsupported`, `port-protocol-unsupported`, `ports-too-many`,
|
|
1379
1425
|
`readiness-target-undeclared`, `environment-too-large`, `no-eligible-node`, `replica-limit`,
|
|
1380
1426
|
`node-authority-unsupported`, `network-policy-missing`, `network-policy-mismatch`,
|
|
1381
|
-
`network-workload-pool-missing
|
|
1427
|
+
`network-workload-pool-missing`, `network-workload-pool-exhausted`, `volume-driver-unsupported`,
|
|
1428
|
+
`volume-options-unsupported`, `volume-shared-unsupported` or `volume-placement-unsupported`. The four
|
|
1429
|
+
volume refusals name what a local storage claim cannot express: a volume driver other than a
|
|
1430
|
+
node-local directory, driver or mount options, one volume mounted by more than one replica or
|
|
1431
|
+
service, and a placement that does not pin each volume to one node. `ports-too-many` is a service declaring more target ports than the
|
|
1382
1432
|
`runtimeConfigContract.maximumTargetPorts` (64) a config carries, where `port-protocol-unsupported`
|
|
1383
1433
|
is about one port's protocol; `node-authority-unsupported` is a node whose session resolved fewer
|
|
1384
1434
|
workload authorities than the spec needs, refused at the admit step rather than handed to a node
|
|
@@ -1881,6 +1931,47 @@ storage provider's binding identity/generation, selected policy, matching observ
|
|
|
1881
1931
|
join: callers still authenticate Cloudly and the provider, check current readiness and retain the
|
|
1882
1932
|
native fence.
|
|
1883
1933
|
|
|
1934
|
+
### Runtime local storage claims
|
|
1935
|
+
|
|
1936
|
+
`data.IRuntimeLocalStorageClaim` is a controller's authority for one retained, node-local volume of
|
|
1937
|
+
one service. Cloudly and Onebox issue it alike (`controller.kind` is `cloudly` or `onebox`). It names
|
|
1938
|
+
the claim revision (`id`, `generation`, `previous`), the organization, cluster, node, runtime
|
|
1939
|
+
namespace, service and the service's own `volumeId`, and it states:
|
|
1940
|
+
|
|
1941
|
+
- `source`: `{ kind: 'directory' }`, a node-local directory the node derives from the claim's
|
|
1942
|
+
identity under a root only the node knows. Network filesystems join this union later as further
|
|
1943
|
+
kinds, with their credentials delivered as secrets and never inside a claim;
|
|
1944
|
+
- `initialization`: `empty` creates an empty root with `initialOwnership { uid, gid, mode }`, which
|
|
1945
|
+
an `empty` claim must state, so a workload without `runAs` never meets a root-owned directory by
|
|
1946
|
+
accident; `import` waits for the volume's bytes to be imported and states `initialOwnership: null`,
|
|
1947
|
+
because an imported tree keeps its own ownership. `mode` holds directory permission bits only
|
|
1948
|
+
(`runtimeLocalStorageClaimContract.directoryModeBits`, `0o3777`; setuid is refused);
|
|
1949
|
+
- `mount { mountPath, readOnly }`, judged by the storage request's own mount path rule: canonical,
|
|
1950
|
+
absolute, not `/`, and never overlapping `/run/secrets`;
|
|
1951
|
+
- `accessMode: 'ReadWriteOnce'`, `reclaimPolicy: 'retain'`, and `lifecycle`: `bound` keeps the
|
|
1952
|
+
volume, `purge` orders its bytes deleted and always follows a bound generation of the same claim.
|
|
1953
|
+
|
|
1954
|
+
The claim carries no host path, device, mount option or credential.
|
|
1955
|
+
`snapshotRuntimeLocalStorageClaim` makes a detached exact-schema snapshot,
|
|
1956
|
+
`computeRuntimeLocalStorageClaimDigest` hashes it under `serve.zone/runtime-local-storage-claim`,
|
|
1957
|
+
`validateRuntimeLocalStorageClaim` verifies the stated digest, and
|
|
1958
|
+
`getRuntimeLocalStorageClaimReference` is the `{ id, generation, digest }` an assignment seals in
|
|
1959
|
+
`workload.storage`. `bindRuntimeLocalStorageClaimToAssignment(claim, assignment, expectedClusterId)`
|
|
1960
|
+
checks that reference and the exact controller, organization, node, runtime namespace, service and
|
|
1961
|
+
cluster; a `purge` claim binds to no assignment, because no workload may mount a volume being deleted.
|
|
1962
|
+
|
|
1963
|
+
`requests.runtimesession.IReq_Controller_Pallet_ApplyRuntimeLocalStorageClaim`
|
|
1964
|
+
(`applyRuntimeLocalStorageClaim`) pushes one claim over the node's authenticated session:
|
|
1965
|
+
`{ session, claim }` → `{ status, state, claim }`. `status` is `accepted`, `replay` or `historical`;
|
|
1966
|
+
`state` is `ready`, `import-required`, `purged` or `uncertain` (a volume the node can vouch for
|
|
1967
|
+
neither way, which it neither mounts nor deletes until an operator decides); `claim` is the sent
|
|
1968
|
+
claim's reference. `bindApplyRuntimeLocalStorageClaimRequest(request, trustedSession)` requires the
|
|
1969
|
+
request's session to equal the node's current one and the claim to name its controller, cluster,
|
|
1970
|
+
node and runtime namespace. `bindApplyRuntimeLocalStorageClaimResponse(response, request)` binds the
|
|
1971
|
+
answer to the sent claim and, for `accepted` and `replay`, to a state the claim can be in: a bound
|
|
1972
|
+
claim is `ready`, `import-required` (an `import` claim only) or `uncertain`, a purge is `purged` or
|
|
1973
|
+
`uncertain`. A `historical` answer describes the newer generation the node holds.
|
|
1974
|
+
|
|
1884
1975
|
`platform.storagemigration` defines the provider-neutral cutover contract for a
|
|
1885
1976
|
named object-storage binding. Corestore atomically owns and fences the source
|
|
1886
1977
|
binding after validating a distinct, unfenced active-object snapshot. Onebox
|
|
@@ -2440,6 +2531,9 @@ credential is excluded.
|
|
|
2440
2531
|
| `registerClusterIngress` | ingress → relay → Cloudly | `{ bearer, version, nodeId, capabilities?, protocol }` → `{ accepted: true, revision \| null, protocol }` |
|
|
2441
2532
|
| `getClusterIngressRouteTable` | ingress → relay | `{ knownRevision \| null }` → `{ table \| null }` |
|
|
2442
2533
|
| `getClusterTrafficStatistics` | Cloudly → relay → ingress | `{ fromDayUtc, toDayUtc, cursor?, limit }` → `{ statistics, nextCursor? }` |
|
|
2534
|
+
| `pushClusterIngressRouteTableChanged` | relay → ingress | `{ revision }` → `{}` |
|
|
2535
|
+
| `designateClusterIngress` | admin → Cloudly | `{ identity, designation }` → `{ designation, spec, replayed }` |
|
|
2536
|
+
| `getClusterIngressDesignation` | admin → Cloudly | `{ identity, clusterId }` → `{ designation \| null }` |
|
|
2443
2537
|
|
|
2444
2538
|
`null` for a table means Cloudly has computed none yet — a fact, not an empty table, and an
|
|
2445
2539
|
ingress must not serve it as one. An ingress announces itself with a **method**, not a connection
|
|
@@ -2465,6 +2559,45 @@ the body in the one server order `protocol.readProtocolOffer` states — the off
|
|
|
2465
2559
|
before this shape check and before the bearer — so a relay refuses a peer it cannot serve without
|
|
2466
2560
|
asking Cloudly, and an accepted registration carries the accepting side's own offer back.
|
|
2467
2561
|
|
|
2562
|
+
A refused registration is named. A protocol mismatch stays the `IProtocolRefusal` every hop already
|
|
2563
|
+
carries; every other refusal travels as `IClusterIngressRegistrationRefusal { code:
|
|
2564
|
+
'cluster-ingress-registration-refused', reason, retryAfterMs }` in `TypedResponseError.errorData`,
|
|
2565
|
+
forwarded by the relay untouched. `clusterIngressRegistrationRefusalReasons` lists the reasons in
|
|
2566
|
+
the order Cloudly judges them: `not-ready` (Cloudly cannot judge registrations yet), `registration-invalid`
|
|
2567
|
+
(the body fails `validateClusterIngressRegistration`), `relay-not-connected` (the forwarding relay is
|
|
2568
|
+
not the connection Cloudly elected for its cluster), `ingress-not-designated`, `node-mismatch` (the
|
|
2569
|
+
node is not one the designation lets the ingress run on), `assignment-not-running` (no ready
|
|
2570
|
+
assignment of the ingress service runs on that node under its current session, or it does not carry
|
|
2571
|
+
the designation's current credential), `bearer-refused` and `conflict` (the designation, assignment
|
|
2572
|
+
or session changed while the registration was judged). `retryAfterMs` is how long the ingress waits
|
|
2573
|
+
before it registers again, 1 s to 300 s (`clusterIngressRegistrationContract`).
|
|
2574
|
+
`validateClusterIngressRegistrationRefusal` names every violation and
|
|
2575
|
+
`readClusterIngressRegistrationRefusal(errorData)` answers the refusal or `null`, reading plain data
|
|
2576
|
+
only, as `protocol.readProtocolRefusal` does. No refusal quotes the bearer.
|
|
2577
|
+
|
|
2578
|
+
`pushClusterIngressRouteTableChanged` is the relay's hint that it holds a newer table. It carries the
|
|
2579
|
+
revision alone (`IClusterIngressRouteTableChange`, `validateClusterIngressRouteTableChange`) and
|
|
2580
|
+
nothing secret; the ingress answers it by pulling `getClusterIngressRouteTable` with the revision it
|
|
2581
|
+
already applied and ignores a hint that is not newer, so a lost or repeated hint costs nothing.
|
|
2582
|
+
|
|
2583
|
+
`clusterIngressEnvironmentNames` spells the ingress workload's environment once for both sides:
|
|
2584
|
+
`SERVEZONE_CLUSTER_INGRESS_BEARER` (the registration bearer, delivered as a secret),
|
|
2585
|
+
`SERVEZONE_CLUSTER_RELAY_ORIGIN` (the exact `IClusterRelay.origin`) and `SERVEZONE_CLUSTER_NODE_ID`
|
|
2586
|
+
(the node the assignment runs on). Cloudly writes them; the ingress reads them.
|
|
2587
|
+
|
|
2588
|
+
A cluster born on Pallet has no cutover scope, census or target set, so its ingress is designated by
|
|
2589
|
+
`IClusterIngressDesignation { id, generation, previous, clusterId, runtime, ingressServiceId,
|
|
2590
|
+
preparedSpecRevision, credential, actorId, acceptedAt, digest }` instead of the fleet-cutover
|
|
2591
|
+
designation. `runtime` is the cluster's `pallet` phase reference and `credential`
|
|
2592
|
+
(`IClusterIngressCredentialReference`) the public identity of the managed bearer, never its value.
|
|
2593
|
+
`snapshotClusterIngressDesignation`, `computeClusterIngressDesignationDigest` (domain
|
|
2594
|
+
`serve.zone/cluster-ingress-designation`) and `validateClusterIngressDesignation` judge it, and
|
|
2595
|
+
`snapshotClusterIngressDesignationRequest` the administrator's compare-and-swap intent
|
|
2596
|
+
(`IClusterIngressDesignationRequest { mutationId, clusterId, ingressServiceId, currentRuntime,
|
|
2597
|
+
expectedSpecRevision, expectedDesignation }`). **A cluster holds exactly one kind of designation**:
|
|
2598
|
+
Cloudly refuses to designate a cluster that holds a fleet-cutover ingress designation, and the
|
|
2599
|
+
reverse. Which nodes the ingress runs on is the ingress service's runtime spec placement.
|
|
2600
|
+
|
|
2468
2601
|
`IClusterTrafficStatistics` is one UTC day of one hostname's traffic on one node —
|
|
2469
2602
|
`requests`, `bytesIn`, `bytesOut`, `connections` and the backend failures the proxy can observe
|
|
2470
2603
|
(`connect`, `handshake`, `request`). Day buckets rather than rates: a rate answers "how busy is it
|
|
@@ -2684,6 +2817,38 @@ const provisioning: requests.gateway.IReq_ProvisionGatewayClientCredential['requ
|
|
|
2684
2817
|
|
|
2685
2818
|
`getGatewayClientContext` returns effective live policy. A `gatewayClient` role necessarily includes the credential ID, bound client ID/type, and `policyGeneration`; consumers should reject admin/operator or mismatched contexts rather than falling back to a caller-supplied owner ID. `getGatewayClientMailOverview` provides an owner-scoped domain and recent-message summary. `getGatewayClientMailDomainCount` derives ownership exclusively from the authenticating gateway credential and returns `{ count: number }` for its distinct configured mail domains. Cloudly therefore uses only `dcrouterGatewayApiToken`; the former `dcrouterOpsApiToken` setting is not part of `data.ICloudlySettings`.
|
|
2686
2819
|
|
|
2820
|
+
### Gateway Client Hostname Records and Certificates
|
|
2821
|
+
|
|
2822
|
+
A gateway client can hold an exact hostname without a gateway route: an address record at a value it chooses, and an exact certificate for that one name. Cloudly uses this for its cluster relay names, whose A record must carry the relay node's own address and whose certificate the relay serves itself. All three requests accept only a gateway-client credential (`identity` is `never`). The gateway resolves the client from that credential, and the hostname must match the client's `hostnamePatterns`. `data.IGatewayClientHostnameOwnership` names only `{ appId, hostname }`. One ownership holds at most one address record and one certificate. A hostname held by another ownership, by a gateway route claim, or by a record the gateway does not manage is a conflict. The gateway never overwrites or adopts it.
|
|
2823
|
+
|
|
2824
|
+
- `syncGatewayClientDnsRecord` (`requests.gateway.IReq_SyncGatewayClientDnsRecord`) publishes, replaces or withdraws the ownership's `A` or `AAAA` record. It needs the token capability `syncDnsRecords`, and the gateway advertises it as `capabilities.dns.clientRecords`. The value must be a canonical unicast literal inside the client's policy field `allowedDnsRecordAddresses`, a list of canonical CIDR prefixes. An absent or empty list allows no record. The result reuses the route DNS vocabulary (`data.TGatewayClientDnsRecordStatus`): `target-invalid` for an address outside the allowance, `record-conflict` with `conflictingRecords` as evidence, and `zone-unavailable` or `mutation-failed` when retryable. Records are never proxied.
|
|
2825
|
+
- `getGatewayClientCertificate` (`requests.gateway.IReq_GetGatewayClientCertificate`) answers `ready` with `data.IGatewayClientCertificate` (PEM chain, PEM private key, `validFrom`, `validUntil`, `renewAfter`), `pending` with `retryAfterMs` while an issuance runs, or `refused` with a `data.TGatewayClientCertificateRefusal`. Asking again after `renewAfter` yields a renewed certificate. It needs `requestCertificates` and `readCertificates`, and the gateway advertises it as `capabilities.certificates.exactIssuance`. The `ready` answer carries a private key; keep it sealed and never log it.
|
|
2826
|
+
- `releaseGatewayClientCertificate` (`requests.gateway.IReq_ReleaseGatewayClientCertificate`) removes the ownership's certificate material and any issuance in flight. It is idempotent and answers `released` or `unchanged`.
|
|
2827
|
+
|
|
2828
|
+
Both sides judge values with the same helpers:
|
|
2829
|
+
|
|
2830
|
+
- `data.validateGatewayClientHostnameOwnership`
|
|
2831
|
+
- `data.validateGatewayClientDnsRecord`
|
|
2832
|
+
- `data.validateGatewayAllowedDnsRecordAddresses`
|
|
2833
|
+
- `data.isGatewayClientDnsRecordAllowed`
|
|
2834
|
+
- `data.isGatewayAddressPrefix`
|
|
2835
|
+
- `data.isGatewayClientHostname`
|
|
2836
|
+
|
|
2837
|
+
Only one spelling of each address is accepted: the dotted quad, or RFC 5952 text for IPv6. `data.gatewayClientHostnameContract` states the bounds: TTL 60 to 86400 seconds with a default of 300, at most 64 allowance prefixes, an `appId` of at most 128 characters, and a hostname of at most 253 characters.
|
|
2838
|
+
|
|
2839
|
+
```typescript
|
|
2840
|
+
import { data, requests } from '@serve.zone/interfaces';
|
|
2841
|
+
|
|
2842
|
+
const record: data.IGatewayClientDnsRecord = { type: 'A', value: '198.51.100.85', ttl: 60 };
|
|
2843
|
+
data.isGatewayClientDnsRecordAllowed(record, ['198.51.100.0/24']); // true
|
|
2844
|
+
|
|
2845
|
+
const request: requests.gateway.IReq_SyncGatewayClientDnsRecord['request'] = {
|
|
2846
|
+
apiToken: gatewayClientToken,
|
|
2847
|
+
ownership: { appId: 'cluster-relay:clusterexample', hostname: 'relay.clusterexample.example.com' },
|
|
2848
|
+
record,
|
|
2849
|
+
};
|
|
2850
|
+
```
|
|
2851
|
+
|
|
2687
2852
|
### CoreMail contracts
|
|
2688
2853
|
|
|
2689
2854
|
`requests.coremail` is the shared contract boundary for authenticated workload
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isPlausibleUnicastIpv4 } from '../private/ipv4.js';
|
|
2
2
|
import { isUtcCalendarDay } from '../private/utccalendarday.js';
|
|
3
3
|
import { validateProtocolOfferOn } from '../protocol.js';
|
|
4
|
-
import { hasExactKeys } from '../private/dataproperties.js';
|
|
4
|
+
import { hasExactKeys, readExactDataRecord } from '../private/dataproperties.js';
|
|
5
5
|
import { isClusterNodeId } from '../private/nodeidentifier.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -424,3 +424,138 @@ export const validateClusterIngressRegistration = (
|
|
|
424
424
|
}
|
|
425
425
|
return errors;
|
|
426
426
|
};
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* The environment variable names of the cluster ingress workload.
|
|
430
|
+
*
|
|
431
|
+
* Cloudly writes them into the ingress workload's assignment environment and the ingress reads
|
|
432
|
+
* them, so both sides spell each name once, here. `bearer` is the credential the ingress presents
|
|
433
|
+
* to `registerClusterIngress`; it is a secret and is delivered as one. `relayOrigin` is the exact
|
|
434
|
+
* `IClusterRelay.origin` of the cluster's relay, and `nodeId` is the node the assignment runs on,
|
|
435
|
+
* which the registration's `nodeId` repeats.
|
|
436
|
+
*/
|
|
437
|
+
export const clusterIngressEnvironmentNames = Object.freeze({
|
|
438
|
+
bearer: 'SERVEZONE_CLUSTER_INGRESS_BEARER',
|
|
439
|
+
relayOrigin: 'SERVEZONE_CLUSTER_RELAY_ORIGIN',
|
|
440
|
+
nodeId: 'SERVEZONE_CLUSTER_NODE_ID',
|
|
441
|
+
} as const);
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Every reason Cloudly refuses a cluster ingress registration, in the order it judges them.
|
|
445
|
+
*
|
|
446
|
+
* A protocol mismatch is not among them: it is refused first, as the `IProtocolRefusal` every
|
|
447
|
+
* TypedRequest hop already carries, and a second name for it would be a second answer to one fact.
|
|
448
|
+
*/
|
|
449
|
+
export const clusterIngressRegistrationRefusalReasons = Object.freeze([
|
|
450
|
+
/** Cloudly cannot judge registrations yet: it is starting, stopping or missing its environment. */
|
|
451
|
+
'not-ready',
|
|
452
|
+
/** The body fails `validateClusterIngressRegistration`. */
|
|
453
|
+
'registration-invalid',
|
|
454
|
+
/** The relay that forwarded the registration is not the connection Cloudly elected for its cluster. */
|
|
455
|
+
'relay-not-connected',
|
|
456
|
+
/** The cluster holds no current ingress designation. */
|
|
457
|
+
'ingress-not-designated',
|
|
458
|
+
/** The named node is not one the designation lets the ingress run on. */
|
|
459
|
+
'node-mismatch',
|
|
460
|
+
/**
|
|
461
|
+
* No ready assignment of the designated ingress service runs on the named node under that node's
|
|
462
|
+
* current session, or the one that runs does not carry the designation's current credential.
|
|
463
|
+
*/
|
|
464
|
+
'assignment-not-running',
|
|
465
|
+
/** The bearer is not the designation's current credential. */
|
|
466
|
+
'bearer-refused',
|
|
467
|
+
/** The designation, the assignment or the session changed while the registration was judged. */
|
|
468
|
+
'conflict',
|
|
469
|
+
] as const);
|
|
470
|
+
|
|
471
|
+
export type TClusterIngressRegistrationRefusalReason = typeof clusterIngressRegistrationRefusalReasons[number];
|
|
472
|
+
|
|
473
|
+
export const clusterIngressRegistrationContract = Object.freeze({
|
|
474
|
+
refusalCode: 'cluster-ingress-registration-refused',
|
|
475
|
+
minimumRetryAfterMs: 1_000,
|
|
476
|
+
maximumRetryAfterMs: 300_000,
|
|
477
|
+
} as const);
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The named refusal of `registerClusterIngress`, carried as `TypedResponseError.errorData` from
|
|
481
|
+
* Cloudly and forwarded by the relay untouched. `retryAfterMs` is how long the ingress waits before
|
|
482
|
+
* it registers again; it never quotes the bearer or anything derived from it.
|
|
483
|
+
*/
|
|
484
|
+
export interface IClusterIngressRegistrationRefusal {
|
|
485
|
+
code: 'cluster-ingress-registration-refused';
|
|
486
|
+
reason: TClusterIngressRegistrationRefusalReason;
|
|
487
|
+
retryAfterMs: number;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const isRetryAfterMs = (valueArg: unknown): valueArg is number => (
|
|
491
|
+
Number.isSafeInteger(valueArg)
|
|
492
|
+
&& (valueArg as number) >= clusterIngressRegistrationContract.minimumRetryAfterMs
|
|
493
|
+
&& (valueArg as number) <= clusterIngressRegistrationContract.maximumRetryAfterMs
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
/** Validates one registration refusal and names every reason it is refused. */
|
|
497
|
+
export const validateClusterIngressRegistrationRefusal = (
|
|
498
|
+
valueArg: unknown,
|
|
499
|
+
pathArg = 'cluster ingress registration refusal',
|
|
500
|
+
): string[] => {
|
|
501
|
+
if (!isRecord(valueArg) || !hasExactKeys(valueArg, ['code', 'reason', 'retryAfterMs'])) {
|
|
502
|
+
return [`${pathArg} must use its exact schema`];
|
|
503
|
+
}
|
|
504
|
+
const errors: string[] = [];
|
|
505
|
+
if (valueArg.code !== clusterIngressRegistrationContract.refusalCode) {
|
|
506
|
+
errors.push(`${pathArg} code must be ${clusterIngressRegistrationContract.refusalCode}`);
|
|
507
|
+
}
|
|
508
|
+
if (!(clusterIngressRegistrationRefusalReasons as readonly unknown[]).includes(valueArg.reason)) {
|
|
509
|
+
errors.push(`${pathArg} reason must be a cluster ingress registration refusal reason`);
|
|
510
|
+
}
|
|
511
|
+
if (!isRetryAfterMs(valueArg.retryAfterMs)) {
|
|
512
|
+
errors.push(`${pathArg} retryAfterMs must be an integer from `
|
|
513
|
+
+ `${clusterIngressRegistrationContract.minimumRetryAfterMs} to `
|
|
514
|
+
+ `${clusterIngressRegistrationContract.maximumRetryAfterMs}`);
|
|
515
|
+
}
|
|
516
|
+
return errors;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* The refusal inside an error payload, or `null` when the value is not one.
|
|
521
|
+
*
|
|
522
|
+
* It reads a value that survived serialization, the way `protocol.readProtocolRefusal` does, so an
|
|
523
|
+
* ingress tells a named registration refusal apart from a protocol refusal and every other error.
|
|
524
|
+
*/
|
|
525
|
+
export const readClusterIngressRegistrationRefusal = (
|
|
526
|
+
valueArg: unknown,
|
|
527
|
+
): IClusterIngressRegistrationRefusal | null => {
|
|
528
|
+
const refusal = readExactDataRecord(valueArg, ['code', 'reason', 'retryAfterMs']);
|
|
529
|
+
if (refusal === null || validateClusterIngressRegistrationRefusal(refusal).length > 0) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
return Object.freeze({
|
|
533
|
+
code: clusterIngressRegistrationContract.refusalCode,
|
|
534
|
+
reason: refusal.reason as TClusterIngressRegistrationRefusalReason,
|
|
535
|
+
retryAfterMs: refusal.retryAfterMs as number,
|
|
536
|
+
});
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* The relay's hint that it holds a newer route table.
|
|
541
|
+
*
|
|
542
|
+
* It carries the revision alone and no route, certificate or key, so it is safe to log; the
|
|
543
|
+
* ingress answers it by pulling `getClusterIngressRouteTable` with the revision it already applied,
|
|
544
|
+
* and ignores a hint whose revision is not greater than that one.
|
|
545
|
+
*/
|
|
546
|
+
export interface IClusterIngressRouteTableChange {
|
|
547
|
+
revision: number;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Validates one route-table change hint. */
|
|
551
|
+
export const validateClusterIngressRouteTableChange = (
|
|
552
|
+
valueArg: unknown,
|
|
553
|
+
pathArg = 'cluster ingress route table change',
|
|
554
|
+
): string[] => {
|
|
555
|
+
if (!isRecord(valueArg) || !hasExactKeys(valueArg, ['revision'])) {
|
|
556
|
+
return [`${pathArg} must use its exact schema`];
|
|
557
|
+
}
|
|
558
|
+
return isCounter(valueArg.revision)
|
|
559
|
+
? []
|
|
560
|
+
: [`${pathArg} revision must be a non-negative safe integer`];
|
|
561
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { TSha256Digest } from './immutableimage.js';
|
|
2
|
+
import type { IClusterRuntimeReference } from './clusterruntime.js';
|
|
3
|
+
import { snapshotClusterRuntimeReference } from './clusterruntime.js';
|
|
4
|
+
import type { IRuntimeAuthorityReference } from './runtimeassignment.js';
|
|
5
|
+
import type { ISecretVersionReference } from './secret.js';
|
|
6
|
+
import { captureNetwork, networkCounter, networkDigest, networkHash, networkId, networkRecord, networkReference,
|
|
7
|
+
networkRevision, requireNetwork } from '../private/networkcontract.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The ingress designation of a cluster born on Pallet.
|
|
11
|
+
*
|
|
12
|
+
* A cluster that never ran Swarm has no cutover scope, census or target set, so its ingress is
|
|
13
|
+
* designated by this record instead of `IFleetCutoverIngressDesignation`. A cluster holds exactly
|
|
14
|
+
* one kind of designation at a time: Cloudly refuses to designate a cluster that holds the other
|
|
15
|
+
* kind. Which nodes the ingress runs on is the ingress service's runtime spec placement; the
|
|
16
|
+
* designation names the service and the credential, and nothing else.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Public identity of the active ingress credential generation; it contains no credential value. */
|
|
20
|
+
export interface IClusterIngressCredentialReference {
|
|
21
|
+
credentialId: string;
|
|
22
|
+
generation: number;
|
|
23
|
+
secretVersion: ISecretVersionReference;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Cloudly's accepted designation of one service as a Pallet cluster's ingress. It grants no
|
|
28
|
+
* assignment, session or registration: the ingress still registers with the credential it names.
|
|
29
|
+
*/
|
|
30
|
+
export interface IClusterIngressDesignation {
|
|
31
|
+
id: string;
|
|
32
|
+
generation: number;
|
|
33
|
+
previous: IRuntimeAuthorityReference | null;
|
|
34
|
+
clusterId: string;
|
|
35
|
+
runtime: IClusterRuntimeReference & { phase: 'pallet' };
|
|
36
|
+
ingressServiceId: string;
|
|
37
|
+
/** The ingress service's runtime spec revision the designation was accepted against. */
|
|
38
|
+
preparedSpecRevision: number;
|
|
39
|
+
credential: IClusterIngressCredentialReference;
|
|
40
|
+
actorId: string;
|
|
41
|
+
acceptedAt: number;
|
|
42
|
+
digest: TSha256Digest;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Administrator intent to designate one service as the cluster's ingress. The caller supplies only
|
|
47
|
+
* compare-and-swap inputs; Cloudly owns credential generation and acceptance.
|
|
48
|
+
*/
|
|
49
|
+
export interface IClusterIngressDesignationRequest {
|
|
50
|
+
mutationId: string;
|
|
51
|
+
clusterId: string;
|
|
52
|
+
ingressServiceId: string;
|
|
53
|
+
currentRuntime: IClusterRuntimeReference & { phase: 'pallet' };
|
|
54
|
+
expectedSpecRevision: number;
|
|
55
|
+
/** Null for creation; the exact current designation for rotation or a change of service. */
|
|
56
|
+
expectedDesignation: IRuntimeAuthorityReference | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const clusterIngressDesignationContract = Object.freeze({
|
|
60
|
+
digestDomain: 'serve.zone/cluster-ingress-designation',
|
|
61
|
+
error: 'Cluster ingress designation contract rejected.',
|
|
62
|
+
} as const);
|
|
63
|
+
|
|
64
|
+
const fail = (): never => { throw new Error(clusterIngressDesignationContract.error); };
|
|
65
|
+
const safe = <T>(operationArg: () => T): T => { try { return operationArg(); } catch { return fail(); } };
|
|
66
|
+
|
|
67
|
+
const palletRuntime = (valueArg: unknown, clusterIdArg: string): IClusterRuntimeReference & { phase: 'pallet' } => {
|
|
68
|
+
const runtime = snapshotClusterRuntimeReference(valueArg);
|
|
69
|
+
requireNetwork(runtime.id === clusterIdArg && runtime.phase === 'pallet');
|
|
70
|
+
return { id: runtime.id, phase: 'pallet', generation: runtime.generation };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const credentialOf = (valueArg: unknown): IClusterIngressCredentialReference => {
|
|
74
|
+
const value = networkRecord(valueArg, ['credentialId', 'generation', 'secretVersion']);
|
|
75
|
+
const secretVersion = networkRecord(value.secretVersion, ['secretId', 'secretVersionId']);
|
|
76
|
+
return {
|
|
77
|
+
credentialId: networkId(value.credentialId),
|
|
78
|
+
generation: networkCounter(value.generation),
|
|
79
|
+
secretVersion: {
|
|
80
|
+
secretId: networkId(secretVersion.secretId),
|
|
81
|
+
secretVersionId: networkId(secretVersion.secretVersionId),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Detached exact-schema snapshot of a designation request. */
|
|
87
|
+
export const snapshotClusterIngressDesignationRequest = (
|
|
88
|
+
valueArg: unknown,
|
|
89
|
+
): IClusterIngressDesignationRequest => safe(() => {
|
|
90
|
+
const value = networkRecord(captureNetwork(valueArg), ['mutationId', 'clusterId', 'ingressServiceId',
|
|
91
|
+
'currentRuntime', 'expectedSpecRevision', 'expectedDesignation']);
|
|
92
|
+
const clusterId = networkId(value.clusterId);
|
|
93
|
+
return {
|
|
94
|
+
mutationId: networkId(value.mutationId),
|
|
95
|
+
clusterId,
|
|
96
|
+
ingressServiceId: networkId(value.ingressServiceId),
|
|
97
|
+
currentRuntime: palletRuntime(value.currentRuntime, clusterId),
|
|
98
|
+
expectedSpecRevision: networkCounter(value.expectedSpecRevision),
|
|
99
|
+
expectedDesignation: value.expectedDesignation === null ? null : networkReference(value.expectedDesignation),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/** Detached exact-schema snapshot. The digest is a separate check. */
|
|
104
|
+
export const snapshotClusterIngressDesignation = (valueArg: unknown): IClusterIngressDesignation => safe(() => {
|
|
105
|
+
const value = networkRecord(captureNetwork(valueArg), ['id', 'generation', 'previous', 'clusterId', 'runtime',
|
|
106
|
+
'ingressServiceId', 'preparedSpecRevision', 'credential', 'actorId', 'acceptedAt', 'digest']);
|
|
107
|
+
const { id, generation, previous } = networkRevision(value);
|
|
108
|
+
const clusterId = networkId(value.clusterId);
|
|
109
|
+
return {
|
|
110
|
+
id,
|
|
111
|
+
generation,
|
|
112
|
+
previous,
|
|
113
|
+
clusterId,
|
|
114
|
+
runtime: palletRuntime(value.runtime, clusterId),
|
|
115
|
+
ingressServiceId: networkId(value.ingressServiceId),
|
|
116
|
+
preparedSpecRevision: networkCounter(value.preparedSpecRevision),
|
|
117
|
+
credential: credentialOf(value.credential),
|
|
118
|
+
actorId: networkId(value.actorId),
|
|
119
|
+
acceptedAt: networkCounter(value.acceptedAt),
|
|
120
|
+
digest: networkDigest(value.digest),
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export const computeClusterIngressDesignationDigest = async (valueArg: unknown): Promise<TSha256Digest> => {
|
|
125
|
+
const designation = snapshotClusterIngressDesignation(valueArg);
|
|
126
|
+
try {
|
|
127
|
+
return await networkHash(clusterIngressDesignationContract.digestDomain, designation);
|
|
128
|
+
} catch {
|
|
129
|
+
return fail();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const validateClusterIngressDesignation = async (valueArg: unknown): Promise<string[]> => {
|
|
134
|
+
try {
|
|
135
|
+
const designation = snapshotClusterIngressDesignation(valueArg);
|
|
136
|
+
return designation.digest === await computeClusterIngressDesignationDigest(designation)
|
|
137
|
+
? [] : ['cluster ingress designation digest does not match'];
|
|
138
|
+
} catch {
|
|
139
|
+
return ['cluster ingress designation must use its exact canonical schema'];
|
|
140
|
+
}
|
|
141
|
+
};
|