@serve.zone/interfaces 31.9.0 → 32.0.1
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 +476 -0
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/appstore/types.d.ts +4 -6
- package/dist_ts/appstore/types.js +5 -5
- package/dist_ts/data/backup.d.ts +3 -26
- package/dist_ts/data/cluster.d.ts +36 -11
- package/dist_ts/data/cluster.js +33 -22
- package/dist_ts/data/clusteringress.d.ts +8 -0
- package/dist_ts/data/clusteringress.js +13 -8
- package/dist_ts/data/clusternode.d.ts +121 -74
- package/dist_ts/data/clusternode.js +109 -132
- package/dist_ts/data/coremail.d.ts +3 -7
- package/dist_ts/data/coremail.js +2 -2
- package/dist_ts/data/coremail.runtime.d.ts +0 -1
- package/dist_ts/data/coremail.runtime.js +5 -22
- package/dist_ts/data/deploymentadoption.d.ts +0 -2
- package/dist_ts/data/deploymentadoption.js +40 -6
- package/dist_ts/data/deploymentoperation.js +16 -3
- package/dist_ts/data/deploymentpreflight.d.ts +0 -2
- package/dist_ts/data/deploymentpreflight.js +1 -1
- package/dist_ts/data/immutableimage.d.ts +38 -21
- package/dist_ts/data/immutableimage.js +45 -5
- package/dist_ts/data/index.d.ts +1 -1
- package/dist_ts/data/isolatedrestore.d.ts +139 -21
- package/dist_ts/data/isolatedrestore.golden.d.ts +0 -1
- package/dist_ts/data/isolatedrestore.golden.js +6 -9
- package/dist_ts/data/isolatedrestore.js +176 -40
- package/dist_ts/data/mail.d.ts +1 -1
- package/dist_ts/data/networkaddressplan.d.ts +8 -8
- package/dist_ts/data/networkaddressplan.js +4 -4
- package/dist_ts/data/networkauthority.d.ts +20 -22
- package/dist_ts/data/networkauthority.js +26 -28
- package/dist_ts/data/networkcredential.d.ts +10 -10
- package/dist_ts/data/networkcredential.js +16 -16
- package/dist_ts/data/networkdelivery.d.ts +18 -18
- package/dist_ts/data/networkdelivery.js +16 -16
- package/dist_ts/data/networklease.d.ts +18 -7
- package/dist_ts/data/networklease.js +29 -7
- package/dist_ts/data/networknode.d.ts +2 -2
- package/dist_ts/data/networkprojection.d.ts +59 -60
- package/dist_ts/data/networkprojection.js +53 -57
- package/dist_ts/data/networkprotection.d.ts +27 -28
- package/dist_ts/data/networkprotection.js +36 -36
- package/dist_ts/data/networkrouter.d.ts +11 -11
- package/dist_ts/data/networkrouter.js +1 -1
- package/dist_ts/data/networksigning.d.ts +16 -18
- package/dist_ts/data/networksigning.js +20 -21
- package/dist_ts/data/nodecredential.d.ts +4 -6
- package/dist_ts/data/nodeenrollment.d.ts +35 -40
- package/dist_ts/data/nodeenrollment.js +57 -72
- package/dist_ts/data/privatenetwork.d.ts +21 -24
- package/dist_ts/data/privatenetwork.js +39 -54
- package/dist_ts/data/runtimeassignment.d.ts +50 -34
- package/dist_ts/data/runtimeassignment.js +79 -45
- package/dist_ts/data/runtimeconfig.d.ts +21 -22
- package/dist_ts/data/runtimeconfig.js +24 -24
- package/dist_ts/data/runtimesession.d.ts +100 -62
- package/dist_ts/data/runtimesession.js +98 -79
- package/dist_ts/data/runtimeterminalreceipt.d.ts +28 -29
- package/dist_ts/data/runtimeterminalreceipt.js +42 -42
- package/dist_ts/data/secret.d.ts +18 -25
- package/dist_ts/data/secret.js +12 -52
- package/dist_ts/data/servicecredential.d.ts +14 -16
- package/dist_ts/data/servicecredential.js +17 -28
- package/dist_ts/data/serviceruntime.d.ts +7 -7
- package/dist_ts/data/serviceruntime.js +4 -4
- package/dist_ts/data/sparknodereport.d.ts +27 -14
- package/dist_ts/data/sparknodereport.js +35 -28
- package/dist_ts/data/webpush.d.ts +0 -1
- package/dist_ts/index.d.ts +2 -1
- package/dist_ts/index.js +3 -2
- package/dist_ts/platform/objectstorageretention.d.ts +21 -24
- package/dist_ts/platform/objectstorageretention.js +10 -17
- package/dist_ts/platform/storage.d.ts +1 -3
- package/dist_ts/platform/storagemigration.d.ts +0 -7
- package/dist_ts/platform/storagemigration.golden.d.ts +0 -1
- package/dist_ts/platform/storagemigration.golden.js +8 -12
- package/dist_ts/platform/storagemigration.js +6 -51
- package/dist_ts/platform/types.d.ts +2 -2
- package/dist_ts/private/canonicaljson.d.ts +9 -1
- package/dist_ts/private/canonicaljson.js +14 -2
- package/dist_ts/private/dataproperties.d.ts +62 -0
- package/dist_ts/private/dataproperties.js +117 -0
- package/dist_ts/private/networkcontract.d.ts +10 -10
- package/dist_ts/private/networkcontract.js +1 -1
- package/dist_ts/private/releaseversion.d.ts +37 -0
- package/dist_ts/private/releaseversion.js +48 -0
- package/dist_ts/private/runtimepolicy.d.ts +2 -2
- package/dist_ts/private/runtimepolicy.js +1 -1
- package/dist_ts/private/tokenlist.d.ts +9 -0
- package/dist_ts/private/tokenlist.js +23 -0
- package/dist_ts/protocol.d.ts +137 -0
- package/dist_ts/protocol.js +255 -0
- package/dist_ts/requests/admin.d.ts +8 -8
- package/dist_ts/requests/backup.d.ts +102 -112
- package/dist_ts/requests/cluster.d.ts +10 -10
- package/dist_ts/requests/clusteringress.d.ts +9 -0
- package/dist_ts/requests/clusteringress.js +1 -1
- package/dist_ts/requests/config.d.ts +3 -3
- package/dist_ts/requests/corestore.d.ts +0 -3
- package/dist_ts/requests/corestore.js +5 -9
- package/dist_ts/requests/network.d.ts +30 -30
- package/dist_ts/requests/node.d.ts +15 -28
- package/dist_ts/requests/pallet.d.ts +13 -13
- package/dist_ts/requests/runtimesession.d.ts +53 -41
- package/dist_ts/requests/secret.d.ts +4 -5
- package/dist_ts/requests/secret.js +1 -5
- package/dist_ts/runtime.corestore.d.ts +56 -74
- package/dist_ts/runtime.corestore.databaseexportpackage.d.ts +30 -36
- package/dist_ts/runtime.corestore.databaseexportpackage.golden.d.ts +11 -12
- package/dist_ts/runtime.corestore.databaseexportpackage.golden.js +54 -71
- package/dist_ts/runtime.corestore.databaseexportpackage.js +9 -25
- package/dist_ts/runtime.corestore.js +35 -135
- package/dist_ts/runtime.corestore.relay.js +2 -7
- package/dist_ts/runtime.d.ts +38 -48
- package/dist_ts/runtime.js +20 -77
- package/dist_ts/runtime.networkprojection.d.ts +4 -4
- package/dist_ts/runtime.networkprojection.js +18 -18
- package/dist_ts/runtime.workloadinit.d.ts +23 -24
- package/dist_ts/runtime.workloadinit.js +6 -13
- package/package.json +2 -2
- package/readme.md +421 -270
- package/ts/00_commitinfo_data.ts +3 -3
- package/ts/appstore/types.ts +4 -6
- package/ts/data/backup.ts +3 -38
- package/ts/data/cluster.ts +51 -34
- package/ts/data/clusteringress.ts +12 -12
- package/ts/data/clusternode.ts +204 -232
- package/ts/data/coremail.runtime.ts +4 -21
- package/ts/data/coremail.ts +3 -7
- package/ts/data/deploymentadoption.ts +46 -4
- package/ts/data/deploymentoperation.ts +16 -1
- package/ts/data/deploymentpreflight.ts +0 -2
- package/ts/data/immutableimage.ts +71 -24
- package/ts/data/index.ts +1 -1
- package/ts/data/isolatedrestore.golden.ts +5 -9
- package/ts/data/isolatedrestore.ts +339 -54
- package/ts/data/mail.ts +1 -1
- package/ts/data/networkaddressplan.ts +13 -13
- package/ts/data/networkauthority.ts +42 -46
- package/ts/data/networkcredential.ts +24 -24
- package/ts/data/networkdelivery.ts +27 -27
- package/ts/data/networklease.ts +40 -13
- package/ts/data/networknode.ts +2 -2
- package/ts/data/networkprojection.ts +119 -124
- package/ts/data/networkprotection.ts +54 -55
- package/ts/data/networkrouter.ts +14 -14
- package/ts/data/networksigning.ts +29 -32
- package/ts/data/nodecredential.ts +4 -6
- package/ts/data/nodeenrollment.ts +96 -114
- package/ts/data/privatenetwork.ts +58 -74
- package/ts/data/runtimeassignment.ts +143 -94
- package/ts/data/runtimeconfig.ts +45 -46
- package/ts/data/runtimesession.ts +185 -138
- package/ts/data/runtimeterminalreceipt.ts +72 -73
- package/ts/data/secret.ts +28 -76
- package/ts/data/servicecredential.ts +33 -43
- package/ts/data/serviceruntime.ts +11 -11
- package/ts/data/sparknodereport.ts +48 -36
- package/ts/data/webpush.ts +0 -1
- package/ts/index.ts +2 -0
- package/ts/platform/objectstorageretention.ts +40 -54
- package/ts/platform/storage.ts +1 -3
- package/ts/platform/storagemigration.golden.ts +7 -11
- package/ts/platform/storagemigration.ts +5 -55
- package/ts/platform/types.ts +2 -2
- package/ts/private/canonicaljson.ts +13 -1
- package/ts/private/dataproperties.ts +139 -0
- package/ts/private/networkcontract.ts +9 -9
- package/ts/private/releaseversion.ts +70 -0
- package/ts/private/runtimepolicy.ts +2 -2
- package/ts/private/tokenlist.ts +30 -0
- package/ts/protocol.ts +364 -0
- package/ts/requests/admin.ts +11 -11
- package/ts/requests/backup.ts +112 -117
- package/ts/requests/cluster.ts +13 -13
- package/ts/requests/clusteringress.ts +9 -0
- package/ts/requests/config.ts +3 -3
- package/ts/requests/corestore.ts +4 -16
- package/ts/requests/network.ts +40 -40
- package/ts/requests/node.ts +21 -38
- package/ts/requests/pallet.ts +17 -17
- package/ts/requests/runtimesession.ts +63 -51
- package/ts/requests/secret.ts +5 -10
- package/ts/runtime.corestore.databaseexportpackage.golden.ts +77 -95
- package/ts/runtime.corestore.databaseexportpackage.ts +51 -73
- package/ts/runtime.corestore.relay.ts +1 -10
- package/ts/runtime.corestore.ts +118 -268
- package/ts/runtime.networkprojection.ts +30 -30
- package/ts/runtime.ts +83 -158
- package/ts/runtime.workloadinit.ts +36 -48
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.d.ts +0 -902
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.golden.d.ts +0 -35
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.golden.js +0 -315
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.js +0 -3315
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.d.ts +0 -291
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.golden.d.ts +0 -30
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.golden.js +0 -408
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.js +0 -1126
- package/ts/runtime.cloudlylegacydeploymentsettlement.golden.ts +0 -421
- package/ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.golden.ts +0 -510
- package/ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.ts +0 -2470
- package/ts/runtime.cloudlylegacydeploymentsettlement.ts +0 -6630
package/ts/data/clusternode.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import type { TSha256Digest, TImmutableContainerPlatform } from './immutableimage.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ISparkNodeReportRecord } from './sparknodereport.js';
|
|
4
|
+
import { protocolRefusalHttpStatus, validateProtocolOfferOn, type IProtocolOffer } from '../protocol.js';
|
|
4
5
|
import {
|
|
5
6
|
canonicalizeStrictJson,
|
|
6
7
|
createCanonicalJsonSha256Hex,
|
|
7
8
|
} from '../private/canonicaljson.js';
|
|
9
|
+
import { hasExactKeys } from '../private/dataproperties.js';
|
|
8
10
|
|
|
9
11
|
export interface IClusterNodeMetrics {
|
|
10
12
|
cpuUsagePercent: number;
|
|
@@ -69,10 +71,85 @@ export interface IServeZoneServiceRuntimeInfo {
|
|
|
69
71
|
error?: string;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* The Swarm-era Spark runtime's own routes.
|
|
76
|
+
*
|
|
77
|
+
* A separate runtime, not an older version of the node runtime: a worker in `coreflow-node` mode
|
|
78
|
+
* posts these four bodies, while a pallet node posts `sparkNodeHeartbeatContract.endpoint`. They
|
|
79
|
+
* retire together with coreflow, not with this release.
|
|
80
|
+
*
|
|
81
|
+
* Each route states its endpoint beside the bytes it reads and writes, in the same three members
|
|
82
|
+
* as `sparkNodeHeartbeatContract`, so one bounded-read helper serves every Spark route. The four
|
|
83
|
+
* bounds differ because the bodies do: a bound is the largest body its own shape can carry here,
|
|
84
|
+
* rounded up, and nothing larger is read before it is refused. Every route is judged in the one
|
|
85
|
+
* server order `protocol.readProtocolOffer` states, against this server's `sparkSwarmNode` offer;
|
|
86
|
+
* none of them carries a typed error, so a refusal is answered with `protocolRefusalStatus` and an
|
|
87
|
+
* `IProtocolRefusal` body before the node is authenticated and before the body is acted on. Every
|
|
88
|
+
* answer states this server's own offer, exactly as the request states the reporter's, so each
|
|
89
|
+
* side judges the other from the bodies they exchanged.
|
|
90
|
+
*/
|
|
91
|
+
export const sparkSwarmNodeContracts = Object.freeze({
|
|
92
|
+
/** The full runtime description on every beat; each described service costs some 600 bytes. */
|
|
93
|
+
heartbeat: Object.freeze({
|
|
94
|
+
endpoint: '/spark/swarm-nodes/heartbeat',
|
|
95
|
+
maxRequestBytes: 32_768,
|
|
96
|
+
maxResponseBytes: 2048,
|
|
97
|
+
} as const),
|
|
98
|
+
/** One sample up, every action the server holds for this node back down. */
|
|
99
|
+
metricsSample: Object.freeze({
|
|
100
|
+
endpoint: '/spark/swarm-nodes/metrics-sample',
|
|
101
|
+
maxRequestBytes: 4096,
|
|
102
|
+
maxResponseBytes: 32_768,
|
|
103
|
+
} as const),
|
|
104
|
+
/** `resultText` is the body here, and a server keeps at most 4000 characters of it. */
|
|
105
|
+
actionResult: Object.freeze({
|
|
106
|
+
endpoint: '/spark/swarm-nodes/action-result',
|
|
107
|
+
maxRequestBytes: 8192,
|
|
108
|
+
maxResponseBytes: 2048,
|
|
109
|
+
} as const),
|
|
110
|
+
/**
|
|
111
|
+
* By far the largest of the four: a manager snapshot carries up to the 1024 observed nodes
|
|
112
|
+
* `validateSparkSwarmObservationRequest` admits, each with two identifiers of up to 200
|
|
113
|
+
* characters, which is close to 600 KiB of body this route must still be able to read.
|
|
114
|
+
*/
|
|
115
|
+
swarmObservation: Object.freeze({
|
|
116
|
+
endpoint: '/spark/swarm-nodes/swarm-observation',
|
|
117
|
+
maxRequestBytes: 786_432,
|
|
118
|
+
maxResponseBytes: 2048,
|
|
119
|
+
} as const),
|
|
120
|
+
protocolRefusalStatus: protocolRefusalHttpStatus,
|
|
121
|
+
} as const);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Spark -> Cloudly body for `sparkSwarmNodeContracts.heartbeat.endpoint`.
|
|
125
|
+
*
|
|
126
|
+
* The host presentation of a Swarm-era worker: its metrics and its runtime description, both
|
|
127
|
+
* stated in full on every beat. This is the one owner of the shape — Cloudly and Spark each
|
|
128
|
+
* carried a private copy until 32.0.0, which is how the two drifted apart.
|
|
129
|
+
*/
|
|
130
|
+
export interface ISparkSwarmNodeHeartbeatRequest {
|
|
131
|
+
nodeId: string;
|
|
132
|
+
nodeToken: string;
|
|
133
|
+
/** Read and negotiated before the node is authenticated; see `sparkSwarmNodeContracts`. */
|
|
134
|
+
protocol: IProtocolOffer;
|
|
135
|
+
metrics: IClusterNodeMetrics;
|
|
136
|
+
runtimeInfo: ISparkNodeRuntimeInfo;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** What Cloudly answers a Swarm-era heartbeat with; `message` explains a refusal to an operator. */
|
|
140
|
+
export interface ISparkSwarmNodeHeartbeatResponse {
|
|
141
|
+
accepted: boolean;
|
|
142
|
+
message?: string;
|
|
143
|
+
/** The answering server's offer; see `sparkSwarmNodeContracts`. */
|
|
144
|
+
protocol: IProtocolOffer;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Spark -> Cloudly body for `sparkSwarmNodeContracts.metricsSample.endpoint`. */
|
|
73
148
|
export interface ISparkMetricsSampleRequest {
|
|
74
149
|
nodeId: string;
|
|
75
150
|
nodeToken: string;
|
|
151
|
+
/** Read and negotiated before the node is authenticated; see `sparkSwarmNodeContracts`. */
|
|
152
|
+
protocol: IProtocolOffer;
|
|
76
153
|
sample: INodeMetricsSample;
|
|
77
154
|
}
|
|
78
155
|
|
|
@@ -81,12 +158,16 @@ export interface ISparkMetricsSampleResponse {
|
|
|
81
158
|
message?: string;
|
|
82
159
|
/** Actions queued for this node; spark executes them and posts results. */
|
|
83
160
|
pendingActions?: INodeAction[];
|
|
161
|
+
/** The answering server's offer; see `sparkSwarmNodeContracts`. */
|
|
162
|
+
protocol: IProtocolOffer;
|
|
84
163
|
}
|
|
85
164
|
|
|
86
|
-
/** Spark -> Cloudly
|
|
165
|
+
/** Spark -> Cloudly body for `sparkSwarmNodeContracts.actionResult.endpoint`. */
|
|
87
166
|
export interface ISparkActionResultRequest {
|
|
88
167
|
nodeId: string;
|
|
89
168
|
nodeToken: string;
|
|
169
|
+
/** Read and negotiated before the node is authenticated; see `sparkSwarmNodeContracts`. */
|
|
170
|
+
protocol: IProtocolOffer;
|
|
90
171
|
actionId: string;
|
|
91
172
|
status: Extract<TNodeActionStatus, 'running' | 'succeeded' | 'failed'>;
|
|
92
173
|
resultText?: string;
|
|
@@ -95,9 +176,11 @@ export interface ISparkActionResultRequest {
|
|
|
95
176
|
export interface ISparkActionResultResponse {
|
|
96
177
|
accepted: boolean;
|
|
97
178
|
message?: string;
|
|
179
|
+
/** The answering server's offer; see `sparkSwarmNodeContracts`. */
|
|
180
|
+
protocol: IProtocolOffer;
|
|
98
181
|
}
|
|
99
182
|
|
|
100
|
-
export interface
|
|
183
|
+
export interface ISwarmManagerObservedNode {
|
|
101
184
|
swarmNodeId: string;
|
|
102
185
|
nodeName: string;
|
|
103
186
|
platform: TImmutableContainerPlatform;
|
|
@@ -110,66 +193,41 @@ export interface ISwarmManagerObservedNodeV1 {
|
|
|
110
193
|
};
|
|
111
194
|
}
|
|
112
195
|
|
|
113
|
-
export interface
|
|
114
|
-
snapshotVersion: 1;
|
|
196
|
+
export interface ISwarmManagerSnapshot {
|
|
115
197
|
snapshotDigest: TSha256Digest;
|
|
116
198
|
nodeCount: number;
|
|
117
199
|
managerCount: number;
|
|
118
|
-
nodes:
|
|
200
|
+
nodes: ISwarmManagerObservedNode[];
|
|
119
201
|
}
|
|
120
202
|
|
|
121
|
-
interface
|
|
122
|
-
schemaVersion: 1;
|
|
203
|
+
interface ISparkSwarmObservationBase {
|
|
123
204
|
reporterSessionId: string;
|
|
124
205
|
observationSequence: number;
|
|
125
206
|
observedAt: number;
|
|
126
207
|
}
|
|
127
208
|
|
|
128
|
-
export type
|
|
129
|
-
| (
|
|
209
|
+
export type TSparkSwarmObservation =
|
|
210
|
+
| (ISparkSwarmObservationBase & {
|
|
130
211
|
state: 'unknown';
|
|
131
212
|
reason: 'docker-unavailable' | 'query-failed' | 'not-initialized';
|
|
132
213
|
})
|
|
133
|
-
| (
|
|
214
|
+
| (ISparkSwarmObservationBase & {
|
|
134
215
|
state: 'not-member';
|
|
135
216
|
})
|
|
136
|
-
| (
|
|
137
|
-
state: 'member';
|
|
138
|
-
swarmClusterId: string;
|
|
139
|
-
localSwarmNodeId: string;
|
|
140
|
-
controlAvailable: boolean;
|
|
141
|
-
managerSnapshot?: ISwarmManagerSnapshotV1;
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
interface ISparkSwarmObservationBaseV2 {
|
|
145
|
-
schemaVersion: 2;
|
|
146
|
-
reporterSessionId: string;
|
|
147
|
-
observationSequence: number;
|
|
148
|
-
observedAt: number;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export type TSparkSwarmObservationV2 =
|
|
152
|
-
| (ISparkSwarmObservationBaseV2 & {
|
|
153
|
-
state: 'unknown';
|
|
154
|
-
reason: 'docker-unavailable' | 'query-failed' | 'not-initialized';
|
|
155
|
-
})
|
|
156
|
-
| (ISparkSwarmObservationBaseV2 & {
|
|
157
|
-
state: 'not-member';
|
|
158
|
-
})
|
|
159
|
-
| (ISparkSwarmObservationBaseV2 & {
|
|
217
|
+
| (ISparkSwarmObservationBase & {
|
|
160
218
|
state: 'member';
|
|
161
219
|
localSwarmNodeId: string;
|
|
162
220
|
controlAvailable: false;
|
|
163
221
|
})
|
|
164
|
-
| (
|
|
222
|
+
| (ISparkSwarmObservationBase & {
|
|
165
223
|
state: 'member';
|
|
166
224
|
swarmClusterId: string;
|
|
167
225
|
localSwarmNodeId: string;
|
|
168
226
|
controlAvailable: true;
|
|
169
|
-
managerSnapshot?:
|
|
227
|
+
managerSnapshot?: ISwarmManagerSnapshot;
|
|
170
228
|
});
|
|
171
229
|
|
|
172
|
-
export type
|
|
230
|
+
export type TSparkSwarmObservationRejectionCode =
|
|
173
231
|
| 'sequence-conflict'
|
|
174
232
|
| 'sequence-stale'
|
|
175
233
|
| 'sequence-gap'
|
|
@@ -180,9 +238,8 @@ export type TSparkSwarmObservationV2RejectionCode =
|
|
|
180
238
|
| 'observation-time-not-advancing'
|
|
181
239
|
| 'acceptance-race';
|
|
182
240
|
|
|
183
|
-
export const
|
|
184
|
-
|
|
185
|
-
digestDomain: 'serve.zone/spark-swarm-observation-v2',
|
|
241
|
+
export const sparkSwarmObservationContract = Object.freeze({
|
|
242
|
+
digestDomain: 'serve.zone/spark-swarm-observation',
|
|
186
243
|
maximumPastAgeMs: 300_000,
|
|
187
244
|
maximumFutureSkewMs: 30_000,
|
|
188
245
|
retainedRetiredSessionIds: 32,
|
|
@@ -196,18 +253,19 @@ export const sparkSwarmObservationV2Contract = Object.freeze({
|
|
|
196
253
|
'observation-from-future': false,
|
|
197
254
|
'observation-time-not-advancing': false,
|
|
198
255
|
'acceptance-race': true,
|
|
199
|
-
} satisfies Record<
|
|
256
|
+
} satisfies Record<TSparkSwarmObservationRejectionCode, boolean>),
|
|
200
257
|
} as const);
|
|
201
258
|
|
|
202
259
|
/** Cloudly derives node and cluster scope from this authenticated Spark request. */
|
|
203
|
-
export interface
|
|
260
|
+
export interface ISparkSwarmObservationRequest {
|
|
204
261
|
nodeId: string;
|
|
205
262
|
nodeToken: string;
|
|
206
|
-
|
|
263
|
+
/** Read and negotiated before the node is authenticated; see `sparkSwarmNodeContracts`. */
|
|
264
|
+
protocol: IProtocolOffer;
|
|
265
|
+
observation: TSparkSwarmObservation;
|
|
207
266
|
}
|
|
208
267
|
|
|
209
|
-
export interface
|
|
210
|
-
schemaVersion: 2;
|
|
268
|
+
export interface ISparkSwarmObservationAcceptedReceipt {
|
|
211
269
|
state: 'accepted';
|
|
212
270
|
nodeId: string;
|
|
213
271
|
reporterSessionId: string;
|
|
@@ -216,18 +274,21 @@ export interface ISparkSwarmObservationV2AcceptedReceipt {
|
|
|
216
274
|
acceptedAt: number;
|
|
217
275
|
/** Runtime target generation observed immediately after this acceptance. */
|
|
218
276
|
targetGenerationAtAcceptance: number;
|
|
277
|
+
/** The answering server's offer; see `sparkSwarmNodeContracts`. */
|
|
278
|
+
protocol: IProtocolOffer;
|
|
219
279
|
}
|
|
220
280
|
|
|
221
|
-
export interface
|
|
222
|
-
schemaVersion: 2;
|
|
281
|
+
export interface ISparkSwarmObservationRejectedReceipt {
|
|
223
282
|
state: 'rejected';
|
|
224
283
|
nodeId: string;
|
|
225
284
|
reporterSessionId: string;
|
|
226
285
|
observationSequence: number;
|
|
227
286
|
observationDigest: TSha256Digest;
|
|
228
287
|
rejectedAt: number;
|
|
229
|
-
code:
|
|
288
|
+
code: TSparkSwarmObservationRejectionCode;
|
|
230
289
|
retryable: boolean;
|
|
290
|
+
/** The answering server's offer; see `sparkSwarmNodeContracts`. */
|
|
291
|
+
protocol: IProtocolOffer;
|
|
231
292
|
}
|
|
232
293
|
|
|
233
294
|
/**
|
|
@@ -235,25 +296,11 @@ export interface ISparkSwarmObservationV2RejectedReceipt {
|
|
|
235
296
|
* authentication. Malformed and authentication failures use generic HTTP
|
|
236
297
|
* errors so they cannot expose credential or persisted replay state.
|
|
237
298
|
*/
|
|
238
|
-
export type
|
|
239
|
-
|
|
|
240
|
-
|
|
|
241
|
-
|
|
242
|
-
/** Cloudly derives node and cluster scope from this authenticated Spark request. */
|
|
243
|
-
export interface ISparkSwarmObservationRequest {
|
|
244
|
-
nodeId: string;
|
|
245
|
-
nodeToken: string;
|
|
246
|
-
observation: TSparkSwarmObservationV1;
|
|
247
|
-
}
|
|
299
|
+
export type TSparkSwarmObservationResponse =
|
|
300
|
+
| ISparkSwarmObservationAcceptedReceipt
|
|
301
|
+
| ISparkSwarmObservationRejectedReceipt;
|
|
248
302
|
|
|
249
|
-
export interface
|
|
250
|
-
accepted: boolean;
|
|
251
|
-
acceptedSequence?: number;
|
|
252
|
-
targetGeneration?: number;
|
|
253
|
-
message?: string;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export interface IClusterRuntimeTargetV1 {
|
|
303
|
+
export interface IClusterRuntimeTarget {
|
|
257
304
|
cloudlyNodeId: string;
|
|
258
305
|
swarmClusterId: string;
|
|
259
306
|
swarmNodeId: string;
|
|
@@ -261,8 +308,7 @@ export interface IClusterRuntimeTargetV1 {
|
|
|
261
308
|
platform: TImmutableContainerPlatform;
|
|
262
309
|
}
|
|
263
310
|
|
|
264
|
-
export interface
|
|
265
|
-
schemaVersion: 1;
|
|
311
|
+
export interface IClusterRuntimeTargetSetReady {
|
|
266
312
|
state: 'ready';
|
|
267
313
|
cloudlyClusterId: string;
|
|
268
314
|
swarmClusterId: string;
|
|
@@ -270,11 +316,10 @@ export interface IClusterRuntimeTargetSetReadyV1 {
|
|
|
270
316
|
targetSetDigest: TSha256Digest;
|
|
271
317
|
acceptedAt: number;
|
|
272
318
|
freshUntil: number;
|
|
273
|
-
targets:
|
|
319
|
+
targets: IClusterRuntimeTarget[];
|
|
274
320
|
}
|
|
275
321
|
|
|
276
|
-
export interface
|
|
277
|
-
schemaVersion: 1;
|
|
322
|
+
export interface IClusterRuntimeTargetSetUnavailable {
|
|
278
323
|
state: 'unavailable';
|
|
279
324
|
cloudlyClusterId: string;
|
|
280
325
|
generation: number;
|
|
@@ -287,9 +332,9 @@ export interface IClusterRuntimeTargetSetUnavailableV1 {
|
|
|
287
332
|
| 'no-schedulable-targets';
|
|
288
333
|
}
|
|
289
334
|
|
|
290
|
-
export type
|
|
291
|
-
|
|
|
292
|
-
|
|
|
335
|
+
export type TClusterRuntimeTargetSet =
|
|
336
|
+
| IClusterRuntimeTargetSetReady
|
|
337
|
+
| IClusterRuntimeTargetSetUnavailable;
|
|
293
338
|
|
|
294
339
|
const runtimeIdentifierRegex = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/;
|
|
295
340
|
const runtimeDigestRegex = /^sha256:[a-f0-9]{64}$/;
|
|
@@ -303,11 +348,6 @@ const isRuntimeRecord = (valueArg: unknown): valueArg is Record<string, unknown>
|
|
|
303
348
|
Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg)
|
|
304
349
|
);
|
|
305
350
|
|
|
306
|
-
const hasExactRuntimeKeys = (
|
|
307
|
-
valueArg: Record<string, unknown>,
|
|
308
|
-
keysArg: string[],
|
|
309
|
-
): boolean => JSON.stringify(Object.keys(valueArg).sort()) === JSON.stringify([...keysArg].sort());
|
|
310
|
-
|
|
311
351
|
const isRuntimeIdentifier = (valueArg: unknown): valueArg is string => (
|
|
312
352
|
typeof valueArg === 'string' && runtimeIdentifierRegex.test(valueArg)
|
|
313
353
|
);
|
|
@@ -326,32 +366,32 @@ const computeSha256 = async (inputArg: string): Promise<TSha256Digest> => {
|
|
|
326
366
|
.join('')}` as TSha256Digest;
|
|
327
367
|
};
|
|
328
368
|
|
|
329
|
-
const
|
|
330
|
-
throw new Error(`Spark Swarm observation
|
|
369
|
+
const failSparkSwarmObservationCanonicalization = (reasonArg: string): never => {
|
|
370
|
+
throw new Error(`Spark Swarm observation cannot be canonicalized: ${reasonArg}`);
|
|
331
371
|
};
|
|
332
372
|
|
|
333
|
-
export const
|
|
334
|
-
observationArg:
|
|
373
|
+
export const createSparkSwarmObservationDigestInput = (
|
|
374
|
+
observationArg: TSparkSwarmObservation,
|
|
335
375
|
): string => canonicalizeStrictJson(
|
|
336
376
|
{
|
|
337
|
-
domain:
|
|
377
|
+
domain: sparkSwarmObservationContract.digestDomain,
|
|
338
378
|
observation: observationArg,
|
|
339
379
|
},
|
|
340
|
-
|
|
341
|
-
'Spark Swarm observation
|
|
380
|
+
failSparkSwarmObservationCanonicalization,
|
|
381
|
+
'Spark Swarm observation digest input',
|
|
342
382
|
);
|
|
343
383
|
|
|
344
|
-
export const
|
|
345
|
-
observationArg:
|
|
384
|
+
export const computeSparkSwarmObservationDigest = async (
|
|
385
|
+
observationArg: TSparkSwarmObservation,
|
|
346
386
|
): Promise<TSha256Digest> => (
|
|
347
387
|
`sha256:${await createCanonicalJsonSha256Hex(
|
|
348
|
-
|
|
349
|
-
|
|
388
|
+
createSparkSwarmObservationDigestInput(observationArg),
|
|
389
|
+
failSparkSwarmObservationCanonicalization,
|
|
350
390
|
)}` as TSha256Digest
|
|
351
391
|
);
|
|
352
392
|
|
|
353
393
|
const canonicalSwarmNode = (
|
|
354
|
-
nodeArg:
|
|
394
|
+
nodeArg: ISwarmManagerObservedNode,
|
|
355
395
|
): Record<string, unknown> => ({
|
|
356
396
|
swarmNodeId: nodeArg.swarmNodeId,
|
|
357
397
|
nodeName: nodeArg.nodeName,
|
|
@@ -369,12 +409,10 @@ const canonicalSwarmNode = (
|
|
|
369
409
|
|
|
370
410
|
export const createSwarmManagerSnapshotDigestInput = (
|
|
371
411
|
swarmClusterIdArg: string,
|
|
372
|
-
snapshotArg:
|
|
412
|
+
snapshotArg: ISwarmManagerSnapshot,
|
|
373
413
|
): string => JSON.stringify({
|
|
374
|
-
schemaVersion: 1,
|
|
375
414
|
purpose: 'serve.zone/swarm-manager-snapshot',
|
|
376
415
|
swarmClusterId: swarmClusterIdArg,
|
|
377
|
-
snapshotVersion: snapshotArg.snapshotVersion,
|
|
378
416
|
nodeCount: snapshotArg.nodeCount,
|
|
379
417
|
managerCount: snapshotArg.managerCount,
|
|
380
418
|
nodes: snapshotArg.nodes.map(canonicalSwarmNode),
|
|
@@ -382,7 +420,7 @@ export const createSwarmManagerSnapshotDigestInput = (
|
|
|
382
420
|
|
|
383
421
|
export const computeSwarmManagerSnapshotDigest = async (
|
|
384
422
|
swarmClusterIdArg: string,
|
|
385
|
-
snapshotArg:
|
|
423
|
+
snapshotArg: ISwarmManagerSnapshot,
|
|
386
424
|
): Promise<TSha256Digest> => computeSha256(
|
|
387
425
|
createSwarmManagerSnapshotDigestInput(swarmClusterIdArg, snapshotArg),
|
|
388
426
|
);
|
|
@@ -393,8 +431,7 @@ const validateSwarmManagerSnapshot = async (
|
|
|
393
431
|
snapshotArg: unknown,
|
|
394
432
|
): Promise<string[]> => {
|
|
395
433
|
if (!isRuntimeRecord(snapshotArg)
|
|
396
|
-
|| !
|
|
397
|
-
'snapshotVersion',
|
|
434
|
+
|| !hasExactKeys(snapshotArg, [
|
|
398
435
|
'snapshotDigest',
|
|
399
436
|
'nodeCount',
|
|
400
437
|
'managerCount',
|
|
@@ -403,9 +440,6 @@ const validateSwarmManagerSnapshot = async (
|
|
|
403
440
|
return ['Spark Swarm manager snapshot must use its exact schema'];
|
|
404
441
|
}
|
|
405
442
|
const errors: string[] = [];
|
|
406
|
-
if (snapshotArg.snapshotVersion !== 1) {
|
|
407
|
-
errors.push('Spark Swarm manager snapshotVersion must be 1');
|
|
408
|
-
}
|
|
409
443
|
if (typeof snapshotArg.snapshotDigest !== 'string'
|
|
410
444
|
|| !runtimeDigestRegex.test(snapshotArg.snapshotDigest)) {
|
|
411
445
|
errors.push('Spark Swarm manager snapshot digest must be canonical');
|
|
@@ -440,7 +474,7 @@ const validateSwarmManagerSnapshot = async (
|
|
|
440
474
|
'state',
|
|
441
475
|
...(isManager ? ['managerStatus'] : []),
|
|
442
476
|
];
|
|
443
|
-
if (!
|
|
477
|
+
if (!hasExactKeys(nodeArg, expectedKeys)) {
|
|
444
478
|
errors.push(`Spark Swarm manager snapshot nodes[${index}] must use its exact role schema`);
|
|
445
479
|
}
|
|
446
480
|
if (!isRuntimeIdentifier(nodeArg.swarmNodeId) || !isRuntimeIdentifier(nodeArg.nodeName)) {
|
|
@@ -457,7 +491,7 @@ const validateSwarmManagerSnapshot = async (
|
|
|
457
491
|
if (isManager) {
|
|
458
492
|
managerCount++;
|
|
459
493
|
if (!isRuntimeRecord(nodeArg.managerStatus)
|
|
460
|
-
|| !
|
|
494
|
+
|| !hasExactKeys(nodeArg.managerStatus, ['leader', 'reachability'])
|
|
461
495
|
|| typeof nodeArg.managerStatus.leader !== 'boolean'
|
|
462
496
|
|| !['unknown', 'unreachable', 'reachable']
|
|
463
497
|
.includes(nodeArg.managerStatus.reachability as string)) {
|
|
@@ -504,7 +538,7 @@ const validateSwarmManagerSnapshot = async (
|
|
|
504
538
|
if (errors.length === 0
|
|
505
539
|
&& snapshotArg.snapshotDigest !== await computeSwarmManagerSnapshotDigest(
|
|
506
540
|
swarmClusterIdArg,
|
|
507
|
-
snapshotArg as unknown as
|
|
541
|
+
snapshotArg as unknown as ISwarmManagerSnapshot,
|
|
508
542
|
)) {
|
|
509
543
|
errors.push('Spark Swarm manager snapshot digest does not match');
|
|
510
544
|
}
|
|
@@ -518,73 +552,6 @@ export const validateSparkSwarmObservation = async (
|
|
|
518
552
|
if (!isRuntimeRecord(observationArg)) return ['Spark Swarm observation must be an object'];
|
|
519
553
|
const observation = observationArg as Record<string, unknown>;
|
|
520
554
|
const commonKeys = [
|
|
521
|
-
'schemaVersion',
|
|
522
|
-
'reporterSessionId',
|
|
523
|
-
'observationSequence',
|
|
524
|
-
'observedAt',
|
|
525
|
-
'state',
|
|
526
|
-
];
|
|
527
|
-
const expectedKeys = observation.state === 'unknown'
|
|
528
|
-
? [...commonKeys, 'reason']
|
|
529
|
-
: observation.state === 'not-member'
|
|
530
|
-
? commonKeys
|
|
531
|
-
: observation.state === 'member'
|
|
532
|
-
? [
|
|
533
|
-
...commonKeys,
|
|
534
|
-
'swarmClusterId',
|
|
535
|
-
'localSwarmNodeId',
|
|
536
|
-
'controlAvailable',
|
|
537
|
-
...(observation.managerSnapshot === undefined ? [] : ['managerSnapshot']),
|
|
538
|
-
]
|
|
539
|
-
: [];
|
|
540
|
-
if (expectedKeys.length === 0 || !hasExactRuntimeKeys(observation, expectedKeys)) {
|
|
541
|
-
return ['Spark Swarm observation must use its exact state schema'];
|
|
542
|
-
}
|
|
543
|
-
const errors: string[] = [];
|
|
544
|
-
if (observation.schemaVersion !== 1) errors.push('Spark Swarm observation schemaVersion must be 1');
|
|
545
|
-
if (!isRuntimeIdentifier(observation.reporterSessionId)
|
|
546
|
-
|| !isPositiveSafeInteger(observation.observationSequence)
|
|
547
|
-
|| !isPositiveSafeInteger(observation.observedAt)) {
|
|
548
|
-
errors.push('Spark Swarm observation identity, sequence, and timestamp must be canonical');
|
|
549
|
-
}
|
|
550
|
-
if (observation.state === 'unknown'
|
|
551
|
-
&& !['docker-unavailable', 'query-failed', 'not-initialized']
|
|
552
|
-
.includes(observation.reason as string)) {
|
|
553
|
-
errors.push('Spark Swarm observation unknown reason must be canonical');
|
|
554
|
-
}
|
|
555
|
-
if (observation.state === 'member') {
|
|
556
|
-
if (!isRuntimeIdentifier(observation.swarmClusterId)
|
|
557
|
-
|| !isRuntimeIdentifier(observation.localSwarmNodeId)
|
|
558
|
-
|| typeof observation.controlAvailable !== 'boolean') {
|
|
559
|
-
errors.push('Spark Swarm member observation must have canonical local authority');
|
|
560
|
-
}
|
|
561
|
-
if (observation.managerSnapshot !== undefined) {
|
|
562
|
-
if (observation.controlAvailable !== true) {
|
|
563
|
-
errors.push('Spark Swarm manager snapshot requires controlAvailable');
|
|
564
|
-
} else if (isRuntimeIdentifier(observation.swarmClusterId)
|
|
565
|
-
&& isRuntimeIdentifier(observation.localSwarmNodeId)) {
|
|
566
|
-
errors.push(...await validateSwarmManagerSnapshot(
|
|
567
|
-
observation.swarmClusterId,
|
|
568
|
-
observation.localSwarmNodeId,
|
|
569
|
-
observation.managerSnapshot,
|
|
570
|
-
));
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
return errors;
|
|
575
|
-
} catch {
|
|
576
|
-
return ['Spark Swarm observation must be safely inspectable'];
|
|
577
|
-
}
|
|
578
|
-
};
|
|
579
|
-
|
|
580
|
-
export const validateSparkSwarmObservationV2 = async (
|
|
581
|
-
observationArg: unknown,
|
|
582
|
-
): Promise<string[]> => {
|
|
583
|
-
try {
|
|
584
|
-
if (!isRuntimeRecord(observationArg)) return ['Spark Swarm observation v2 must be an object'];
|
|
585
|
-
const observation = observationArg as Record<string, unknown>;
|
|
586
|
-
const commonKeys = [
|
|
587
|
-
'schemaVersion',
|
|
588
555
|
'reporterSessionId',
|
|
589
556
|
'observationSequence',
|
|
590
557
|
'observedAt',
|
|
@@ -605,31 +572,28 @@ export const validateSparkSwarmObservationV2 = async (
|
|
|
605
572
|
...(observation.managerSnapshot === undefined ? [] : ['managerSnapshot']),
|
|
606
573
|
]
|
|
607
574
|
: [];
|
|
608
|
-
if (expectedKeys.length === 0 || !
|
|
609
|
-
return ['Spark Swarm observation
|
|
575
|
+
if (expectedKeys.length === 0 || !hasExactKeys(observation, expectedKeys)) {
|
|
576
|
+
return ['Spark Swarm observation must use its exact state schema'];
|
|
610
577
|
}
|
|
611
578
|
const errors: string[] = [];
|
|
612
|
-
if (observation.schemaVersion !== 2) {
|
|
613
|
-
errors.push('Spark Swarm observation v2 schemaVersion must be 2');
|
|
614
|
-
}
|
|
615
579
|
if (!isRuntimeIdentifier(observation.reporterSessionId)
|
|
616
580
|
|| !isPositiveSafeInteger(observation.observationSequence)
|
|
617
581
|
|| !isPositiveSafeInteger(observation.observedAt)) {
|
|
618
|
-
errors.push('Spark Swarm observation
|
|
582
|
+
errors.push('Spark Swarm observation identity, sequence, and timestamp must be canonical');
|
|
619
583
|
}
|
|
620
584
|
if (observation.state === 'unknown'
|
|
621
585
|
&& !['docker-unavailable', 'query-failed', 'not-initialized']
|
|
622
586
|
.includes(observation.reason as string)) {
|
|
623
|
-
errors.push('Spark Swarm observation
|
|
587
|
+
errors.push('Spark Swarm observation unknown reason must be canonical');
|
|
624
588
|
}
|
|
625
589
|
if (observation.state === 'member') {
|
|
626
590
|
if (!isRuntimeIdentifier(observation.localSwarmNodeId)
|
|
627
591
|
|| typeof observation.controlAvailable !== 'boolean') {
|
|
628
|
-
errors.push('Spark Swarm member observation
|
|
592
|
+
errors.push('Spark Swarm member observation must have canonical local authority');
|
|
629
593
|
}
|
|
630
594
|
if (observation.controlAvailable === true) {
|
|
631
595
|
if (!isRuntimeIdentifier(observation.swarmClusterId)) {
|
|
632
|
-
errors.push('Spark Swarm manager observation
|
|
596
|
+
errors.push('Spark Swarm manager observation must have canonical cluster authority');
|
|
633
597
|
} else if (observation.managerSnapshot !== undefined
|
|
634
598
|
&& isRuntimeIdentifier(observation.localSwarmNodeId)) {
|
|
635
599
|
errors.push(...await validateSwarmManagerSnapshot(
|
|
@@ -642,59 +606,62 @@ export const validateSparkSwarmObservationV2 = async (
|
|
|
642
606
|
}
|
|
643
607
|
return errors;
|
|
644
608
|
} catch {
|
|
645
|
-
return ['Spark Swarm observation
|
|
609
|
+
return ['Spark Swarm observation must be safely inspectable'];
|
|
646
610
|
}
|
|
647
611
|
};
|
|
648
612
|
|
|
649
|
-
export const
|
|
613
|
+
export const validateSparkSwarmObservationRequest = async (
|
|
650
614
|
requestArg: unknown,
|
|
651
615
|
): Promise<string[]> => {
|
|
652
616
|
try {
|
|
653
617
|
if (!isRuntimeRecord(requestArg)
|
|
654
|
-
|| !
|
|
655
|
-
return ['Spark Swarm observation
|
|
618
|
+
|| !hasExactKeys(requestArg, ['nodeId', 'nodeToken', 'protocol', 'observation'])) {
|
|
619
|
+
return ['Spark Swarm observation request must use its exact schema'];
|
|
656
620
|
}
|
|
657
621
|
try {
|
|
658
622
|
canonicalizeStrictJson(
|
|
659
623
|
requestArg,
|
|
660
|
-
|
|
661
|
-
'Spark Swarm observation
|
|
624
|
+
failSparkSwarmObservationCanonicalization,
|
|
625
|
+
'Spark Swarm observation request',
|
|
662
626
|
);
|
|
663
627
|
} catch {
|
|
664
|
-
return ['Spark Swarm observation
|
|
628
|
+
return ['Spark Swarm observation request must contain strict canonical JSON data'];
|
|
665
629
|
}
|
|
666
|
-
const errors =
|
|
630
|
+
const errors = validateProtocolOfferOn(
|
|
631
|
+
requestArg,
|
|
632
|
+
'Spark Swarm observation request protocol',
|
|
633
|
+
);
|
|
634
|
+
errors.push(...await validateSparkSwarmObservation(requestArg.observation));
|
|
667
635
|
if (!isRuntimeIdentifier(requestArg.nodeId)) {
|
|
668
|
-
errors.push('Spark Swarm observation
|
|
636
|
+
errors.push('Spark Swarm observation request nodeId must be canonical');
|
|
669
637
|
}
|
|
670
638
|
if (typeof requestArg.nodeToken !== 'string'
|
|
671
639
|
|| requestArg.nodeToken.length < 1
|
|
672
640
|
|| requestArg.nodeToken.length > 512
|
|
673
641
|
|| requestArg.nodeToken.trim() !== requestArg.nodeToken) {
|
|
674
|
-
errors.push('Spark Swarm observation
|
|
642
|
+
errors.push('Spark Swarm observation request nodeToken must be a bounded opaque token');
|
|
675
643
|
}
|
|
676
644
|
return errors;
|
|
677
645
|
} catch {
|
|
678
|
-
return ['Spark Swarm observation
|
|
646
|
+
return ['Spark Swarm observation request must be safely inspectable'];
|
|
679
647
|
}
|
|
680
648
|
};
|
|
681
649
|
|
|
682
|
-
export const
|
|
650
|
+
export const validateSparkSwarmObservationResponse = async (
|
|
683
651
|
responseArg: unknown,
|
|
684
|
-
expectedRequestArg:
|
|
652
|
+
expectedRequestArg: ISparkSwarmObservationRequest,
|
|
685
653
|
): Promise<string[]> => {
|
|
686
654
|
try {
|
|
687
|
-
const requestErrors = await
|
|
655
|
+
const requestErrors = await validateSparkSwarmObservationRequest(
|
|
688
656
|
expectedRequestArg,
|
|
689
657
|
);
|
|
690
658
|
if (requestErrors.length > 0) {
|
|
691
|
-
return ['expected Spark Swarm observation
|
|
659
|
+
return ['expected Spark Swarm observation request must be valid'];
|
|
692
660
|
}
|
|
693
661
|
if (!isRuntimeRecord(responseArg)) {
|
|
694
|
-
return ['Spark Swarm observation
|
|
662
|
+
return ['Spark Swarm observation response must be an object'];
|
|
695
663
|
}
|
|
696
664
|
const acceptedKeys = [
|
|
697
|
-
'schemaVersion',
|
|
698
665
|
'state',
|
|
699
666
|
'nodeId',
|
|
700
667
|
'reporterSessionId',
|
|
@@ -702,9 +669,9 @@ export const validateSparkSwarmObservationV2Response = async (
|
|
|
702
669
|
'observationDigest',
|
|
703
670
|
'acceptedAt',
|
|
704
671
|
'targetGenerationAtAcceptance',
|
|
672
|
+
'protocol',
|
|
705
673
|
];
|
|
706
674
|
const rejectedKeys = [
|
|
707
|
-
'schemaVersion',
|
|
708
675
|
'state',
|
|
709
676
|
'nodeId',
|
|
710
677
|
'reporterSessionId',
|
|
@@ -713,61 +680,71 @@ export const validateSparkSwarmObservationV2Response = async (
|
|
|
713
680
|
'rejectedAt',
|
|
714
681
|
'code',
|
|
715
682
|
'retryable',
|
|
683
|
+
'protocol',
|
|
716
684
|
];
|
|
717
685
|
const expectedKeys = responseArg.state === 'accepted'
|
|
718
686
|
? acceptedKeys
|
|
719
687
|
: responseArg.state === 'rejected'
|
|
720
688
|
? rejectedKeys
|
|
721
689
|
: [];
|
|
722
|
-
if (expectedKeys.length === 0 || !
|
|
723
|
-
return ['Spark Swarm observation
|
|
690
|
+
if (expectedKeys.length === 0 || !hasExactKeys(responseArg, expectedKeys)) {
|
|
691
|
+
return ['Spark Swarm observation response must use its exact state schema'];
|
|
724
692
|
}
|
|
725
693
|
try {
|
|
726
694
|
canonicalizeStrictJson(
|
|
727
695
|
responseArg,
|
|
728
|
-
|
|
729
|
-
'Spark Swarm observation
|
|
696
|
+
failSparkSwarmObservationCanonicalization,
|
|
697
|
+
'Spark Swarm observation response',
|
|
730
698
|
);
|
|
731
699
|
} catch {
|
|
732
|
-
return ['Spark Swarm observation
|
|
700
|
+
return ['Spark Swarm observation response must contain strict canonical JSON data'];
|
|
733
701
|
}
|
|
734
|
-
const errors
|
|
735
|
-
|
|
702
|
+
const errors = validateProtocolOfferOn(
|
|
703
|
+
responseArg,
|
|
704
|
+
'Spark Swarm observation response protocol',
|
|
705
|
+
);
|
|
706
|
+
const observationDigest = await computeSparkSwarmObservationDigest(
|
|
736
707
|
expectedRequestArg.observation,
|
|
737
708
|
);
|
|
738
|
-
if (responseArg.
|
|
739
|
-
|| responseArg.nodeId !== expectedRequestArg.nodeId
|
|
709
|
+
if (responseArg.nodeId !== expectedRequestArg.nodeId
|
|
740
710
|
|| responseArg.reporterSessionId !== expectedRequestArg.observation.reporterSessionId
|
|
741
711
|
|| responseArg.observationSequence !== expectedRequestArg.observation.observationSequence
|
|
742
712
|
|| responseArg.observationDigest !== observationDigest) {
|
|
743
|
-
errors.push('Spark Swarm observation
|
|
713
|
+
errors.push('Spark Swarm observation response must bind the expected request');
|
|
744
714
|
}
|
|
745
715
|
if (responseArg.state === 'accepted') {
|
|
746
716
|
if (!isPositiveSafeInteger(responseArg.acceptedAt)
|
|
747
717
|
|| !Number.isSafeInteger(responseArg.targetGenerationAtAcceptance)
|
|
748
718
|
|| (responseArg.targetGenerationAtAcceptance as number) < 0) {
|
|
749
|
-
errors.push('Spark Swarm observation
|
|
719
|
+
errors.push('Spark Swarm observation accepted receipt fields must be canonical');
|
|
750
720
|
}
|
|
751
721
|
} else {
|
|
752
|
-
const retryability =
|
|
722
|
+
const retryability = sparkSwarmObservationContract.rejectionRetryability;
|
|
753
723
|
if (!isPositiveSafeInteger(responseArg.rejectedAt)
|
|
754
724
|
|| typeof responseArg.code !== 'string'
|
|
755
725
|
|| !Object.hasOwn(retryability, responseArg.code)
|
|
756
726
|
|| responseArg.retryable !== retryability[
|
|
757
|
-
responseArg.code as
|
|
727
|
+
responseArg.code as TSparkSwarmObservationRejectionCode
|
|
758
728
|
]) {
|
|
759
|
-
errors.push('Spark Swarm observation
|
|
729
|
+
errors.push('Spark Swarm observation rejected receipt fields must be canonical');
|
|
760
730
|
}
|
|
761
731
|
}
|
|
762
732
|
return errors;
|
|
763
733
|
} catch {
|
|
764
|
-
return ['Spark Swarm observation
|
|
734
|
+
return ['Spark Swarm observation response must be safely inspectable'];
|
|
765
735
|
}
|
|
766
736
|
};
|
|
767
737
|
|
|
738
|
+
/**
|
|
739
|
+
* The exact five members of a target that the set digest is taken over.
|
|
740
|
+
*
|
|
741
|
+
* A projection, not the canonical-JSON rule: it drops anything else a caller's target carries, so
|
|
742
|
+
* the digest cannot move with a member no peer agreed on. Canonicalizing the target instead would
|
|
743
|
+
* hash whatever it happens to hold.
|
|
744
|
+
*/
|
|
768
745
|
const canonicalRuntimeTarget = (
|
|
769
|
-
targetArg:
|
|
770
|
-
):
|
|
746
|
+
targetArg: IClusterRuntimeTarget,
|
|
747
|
+
): IClusterRuntimeTarget => ({
|
|
771
748
|
cloudlyNodeId: targetArg.cloudlyNodeId,
|
|
772
749
|
swarmClusterId: targetArg.swarmClusterId,
|
|
773
750
|
swarmNodeId: targetArg.swarmNodeId,
|
|
@@ -776,9 +753,8 @@ const canonicalRuntimeTarget = (
|
|
|
776
753
|
});
|
|
777
754
|
|
|
778
755
|
export const createClusterRuntimeTargetSetDigestInput = (
|
|
779
|
-
targetSetArg:
|
|
756
|
+
targetSetArg: IClusterRuntimeTargetSetReady,
|
|
780
757
|
): string => JSON.stringify({
|
|
781
|
-
schemaVersion: targetSetArg.schemaVersion,
|
|
782
758
|
purpose: 'serve.zone/cluster-runtime-target-set',
|
|
783
759
|
cloudlyClusterId: targetSetArg.cloudlyClusterId,
|
|
784
760
|
swarmClusterId: targetSetArg.swarmClusterId,
|
|
@@ -787,7 +763,7 @@ export const createClusterRuntimeTargetSetDigestInput = (
|
|
|
787
763
|
});
|
|
788
764
|
|
|
789
765
|
export const computeClusterRuntimeTargetSetDigest = async (
|
|
790
|
-
targetSetArg:
|
|
766
|
+
targetSetArg: IClusterRuntimeTargetSetReady,
|
|
791
767
|
): Promise<TSha256Digest> => computeSha256(createClusterRuntimeTargetSetDigestInput(targetSetArg));
|
|
792
768
|
|
|
793
769
|
export const validateClusterRuntimeTargetSet = async (
|
|
@@ -796,8 +772,7 @@ export const validateClusterRuntimeTargetSet = async (
|
|
|
796
772
|
try {
|
|
797
773
|
if (!isRuntimeRecord(targetSetArg)) return ['cluster runtime target set must be an object'];
|
|
798
774
|
if (targetSetArg.state === 'unavailable') {
|
|
799
|
-
if (!
|
|
800
|
-
'schemaVersion',
|
|
775
|
+
if (!hasExactKeys(targetSetArg, [
|
|
801
776
|
'state',
|
|
802
777
|
'cloudlyClusterId',
|
|
803
778
|
'generation',
|
|
@@ -807,8 +782,7 @@ export const validateClusterRuntimeTargetSet = async (
|
|
|
807
782
|
return ['unavailable cluster runtime target set must use its exact schema'];
|
|
808
783
|
}
|
|
809
784
|
const errors: string[] = [];
|
|
810
|
-
if (targetSetArg.
|
|
811
|
-
|| !isRuntimeIdentifier(targetSetArg.cloudlyClusterId)
|
|
785
|
+
if (!isRuntimeIdentifier(targetSetArg.cloudlyClusterId)
|
|
812
786
|
|| !Number.isSafeInteger(targetSetArg.generation)
|
|
813
787
|
|| (targetSetArg.generation as number) < 0
|
|
814
788
|
|| !isPositiveSafeInteger(targetSetArg.changedAt)
|
|
@@ -827,8 +801,7 @@ export const validateClusterRuntimeTargetSet = async (
|
|
|
827
801
|
return errors;
|
|
828
802
|
}
|
|
829
803
|
if (targetSetArg.state !== 'ready'
|
|
830
|
-
|| !
|
|
831
|
-
'schemaVersion',
|
|
804
|
+
|| !hasExactKeys(targetSetArg, [
|
|
832
805
|
'state',
|
|
833
806
|
'cloudlyClusterId',
|
|
834
807
|
'swarmClusterId',
|
|
@@ -841,8 +814,7 @@ export const validateClusterRuntimeTargetSet = async (
|
|
|
841
814
|
return ['ready cluster runtime target set must use its exact schema'];
|
|
842
815
|
}
|
|
843
816
|
const errors: string[] = [];
|
|
844
|
-
if (targetSetArg.
|
|
845
|
-
|| !isRuntimeIdentifier(targetSetArg.cloudlyClusterId)
|
|
817
|
+
if (!isRuntimeIdentifier(targetSetArg.cloudlyClusterId)
|
|
846
818
|
|| !isRuntimeIdentifier(targetSetArg.swarmClusterId)
|
|
847
819
|
|| !isPositiveSafeInteger(targetSetArg.generation)
|
|
848
820
|
|| !isPositiveSafeInteger(targetSetArg.acceptedAt)
|
|
@@ -864,7 +836,7 @@ export const validateClusterRuntimeTargetSet = async (
|
|
|
864
836
|
let previousSortKey: string | undefined;
|
|
865
837
|
for (const [index, targetArg] of targetSetArg.targets.entries()) {
|
|
866
838
|
if (!isRuntimeRecord(targetArg)
|
|
867
|
-
|| !
|
|
839
|
+
|| !hasExactKeys(targetArg, [
|
|
868
840
|
'cloudlyNodeId',
|
|
869
841
|
'swarmClusterId',
|
|
870
842
|
'swarmNodeId',
|
|
@@ -899,7 +871,7 @@ export const validateClusterRuntimeTargetSet = async (
|
|
|
899
871
|
}
|
|
900
872
|
if (errors.length === 0
|
|
901
873
|
&& targetSetArg.targetSetDigest !== await computeClusterRuntimeTargetSetDigest(
|
|
902
|
-
targetSetArg as unknown as
|
|
874
|
+
targetSetArg as unknown as IClusterRuntimeTargetSetReady,
|
|
903
875
|
)) {
|
|
904
876
|
errors.push('cluster runtime target set digest does not match');
|
|
905
877
|
}
|
|
@@ -910,9 +882,9 @@ export const validateClusterRuntimeTargetSet = async (
|
|
|
910
882
|
};
|
|
911
883
|
|
|
912
884
|
export const isClusterRuntimeTargetSetFresh = (
|
|
913
|
-
targetSetArg:
|
|
885
|
+
targetSetArg: TClusterRuntimeTargetSet,
|
|
914
886
|
nowArg: number,
|
|
915
|
-
): targetSetArg is
|
|
887
|
+
): targetSetArg is IClusterRuntimeTargetSetReady => (
|
|
916
888
|
targetSetArg.state === 'ready'
|
|
917
889
|
&& Number.isSafeInteger(nowArg)
|
|
918
890
|
&& nowArg >= targetSetArg.acceptedAt
|
|
@@ -994,7 +966,7 @@ export interface IClusterNode {
|
|
|
994
966
|
sparkRuntimeInfo?: ISparkNodeRuntimeInfo;
|
|
995
967
|
|
|
996
968
|
/** Separately accepted Pallet-era host report; never grants workload readiness. */
|
|
997
|
-
sparkNodeReport?:
|
|
969
|
+
sparkNodeReport?: ISparkNodeReportRecord;
|
|
998
970
|
|
|
999
971
|
/**
|
|
1000
972
|
* Docker swarm node ID if part of swarm
|