@serve.zone/interfaces 32.68.0 → 32.69.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 +8 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/clusternode.d.ts +15 -2
- package/dist_ts/data/clusternode.js +1 -1
- package/dist_ts/data/index.d.ts +2 -0
- package/dist_ts/data/index.js +3 -1
- package/dist_ts/data/nodeaction.d.ts +439 -0
- package/dist_ts/data/nodeaction.js +434 -0
- package/dist_ts/data/sparkfleettarget.d.ts +80 -0
- package/dist_ts/data/sparkfleettarget.js +71 -0
- package/dist_ts/data/sparknodereport.d.ts +19 -0
- package/dist_ts/data/sparknodereport.js +21 -4
- package/dist_ts/requests/node.d.ts +100 -0
- package/package.json +1 -1
- package/readme.md +47 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/clusternode.ts +17 -2
- package/ts/data/index.ts +2 -0
- package/ts/data/nodeaction.ts +755 -0
- package/ts/data/sparkfleettarget.ts +138 -0
- package/ts/data/sparknodereport.ts +42 -3
- package/ts/requests/node.ts +118 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { type IProtocolOffer } from '../protocol.js';
|
|
2
|
+
import type { TNodeActionName, TNodeActionStatus } from './clusternode.js';
|
|
3
|
+
/**
|
|
4
|
+
* Every node action, in canonical token order, so a list of them (a Spark's `actionExecutors`)
|
|
5
|
+
* is compared byte for byte.
|
|
6
|
+
*/
|
|
7
|
+
export declare const nodeActionNames: readonly ["reboot", "serveZoneServiceUpdate", "systemUpgrade"];
|
|
8
|
+
export declare const isNodeActionName: (valueArg: unknown) => valueArg is TNodeActionName;
|
|
9
|
+
/**
|
|
10
|
+
* Whether each action state is still open or ended.
|
|
11
|
+
*
|
|
12
|
+
* The four values the Swarm-era carrier shipped keep their meaning: `pending` is the requested
|
|
13
|
+
* state of every action, `running` is a Swarm-era action its daemon started, and `succeeded` and
|
|
14
|
+
* `failed` end one. The node runtime's lifecycle (`INodeActionLifecycle`) adds the rest:
|
|
15
|
+
*
|
|
16
|
+
* - `claimed`: the node's Spark took the action (`claim`), stating the boot it took it in.
|
|
17
|
+
* - `executing`, `draining`, `rebooting`: the phase Spark reports, only ever forward
|
|
18
|
+
* (`sparkNodeActionPhaseOrder`).
|
|
19
|
+
* - `verifying`: Cloudly saw the evidence the action changed the node (`nodeActionVerification`)
|
|
20
|
+
* and waits for the node's runtime to come back.
|
|
21
|
+
* - `expired`: never claimed in its claim window, or a Swarm-era record no carrier can deliver
|
|
22
|
+
* (`TNodeActionExpiryReason`).
|
|
23
|
+
* - `cancelled`: an operator withdrew it while it was still `pending`.
|
|
24
|
+
* - `unverified`: its deadline passed before Cloudly could verify it. An operator judges the node;
|
|
25
|
+
* Cloudly never retries it.
|
|
26
|
+
*/
|
|
27
|
+
export declare const nodeActionStatusKinds: Readonly<{
|
|
28
|
+
readonly pending: 'open';
|
|
29
|
+
readonly running: 'open';
|
|
30
|
+
readonly claimed: 'open';
|
|
31
|
+
readonly executing: 'open';
|
|
32
|
+
readonly draining: 'open';
|
|
33
|
+
readonly rebooting: 'open';
|
|
34
|
+
readonly verifying: 'open';
|
|
35
|
+
readonly succeeded: 'terminal';
|
|
36
|
+
readonly failed: 'terminal';
|
|
37
|
+
readonly expired: 'terminal';
|
|
38
|
+
readonly cancelled: 'terminal';
|
|
39
|
+
readonly unverified: 'terminal';
|
|
40
|
+
}>;
|
|
41
|
+
/** Every open state, the ones the one-open-action-per-node rule counts. */
|
|
42
|
+
export declare const nodeActionOpenStatuses: readonly TNodeActionStatus[];
|
|
43
|
+
export declare const isNodeActionOpen: (statusArg: TNodeActionStatus) => boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The phases Spark reports while it executes a claimed action, in the only order it may report
|
|
46
|
+
* them. A phase may be skipped, never repeated or reported after a later one.
|
|
47
|
+
*/
|
|
48
|
+
export declare const sparkNodeActionPhaseOrder: readonly ["claimed", "executing", "draining", "rebooting"];
|
|
49
|
+
/** A phase Spark reports with a `phase` transition. */
|
|
50
|
+
export type TSparkNodeActionPhase = Exclude<typeof sparkNodeActionPhaseOrder[number], 'claimed'>;
|
|
51
|
+
/**
|
|
52
|
+
* Time budgets of the node runtime's action lifecycle, in milliseconds.
|
|
53
|
+
*
|
|
54
|
+
* A requested action waits `claimWindowMs` for its node's Spark to claim it, then expires. A claimed
|
|
55
|
+
* action must be verified within its completion budget (`nodeActionCompletionBudgetMs`), measured
|
|
56
|
+
* from the claim, or it ends `unverified`. Each budget is the sum of the steps the action takes. One
|
|
57
|
+
* budget is read from Spark's code; every other one is chosen here, and each member says which.
|
|
58
|
+
*/
|
|
59
|
+
export declare const nodeActionContract: Readonly<{
|
|
60
|
+
/** Chosen: how long a requested action waits for its node's Spark to claim it (10 min). */
|
|
61
|
+
readonly claimWindowMs: 600000;
|
|
62
|
+
/**
|
|
63
|
+
* Spark code: `sparkNodeStopTimeoutSeconds`, the node unit's `TimeoutStopSec=` (522 s), in which
|
|
64
|
+
* Pallet stops every workload it runs.
|
|
65
|
+
*/
|
|
66
|
+
readonly drainMs: 522000;
|
|
67
|
+
/** Chosen: a host's reboot until Spark reports from the new boot (10 min). */
|
|
68
|
+
readonly bootMs: 600000;
|
|
69
|
+
/**
|
|
70
|
+
* Chosen: how long Pallet takes to start and register again after the node runtime starts
|
|
71
|
+
* (712 s, from the 2026-11-05 survey; Spark states no start budget of its own).
|
|
72
|
+
*/
|
|
73
|
+
readonly startMs: 712000;
|
|
74
|
+
/** Chosen: downloading and checking the target's node bundle before anything stops (10 min). */
|
|
75
|
+
readonly downloadMs: 600000;
|
|
76
|
+
/** Chosen: the host's package upgrade (30 min). */
|
|
77
|
+
readonly hostUpgradeMs: 1800000;
|
|
78
|
+
/** Entries an action's timeline keeps; each state is entered at most once. */
|
|
79
|
+
readonly maximumTimelineEntries: 16;
|
|
80
|
+
/** Characters of operator text one timeline entry or failure carries. */
|
|
81
|
+
readonly maximumDetailLength: 512;
|
|
82
|
+
/** Services one impact names; a node holding more is answered `impact-unreadable`. */
|
|
83
|
+
readonly maximumImpactServices: 1024;
|
|
84
|
+
/** Characters of one service name an impact carries. */
|
|
85
|
+
readonly maximumServiceNameLength: 256;
|
|
86
|
+
}>;
|
|
87
|
+
/**
|
|
88
|
+
* How long a claimed action has until Cloudly must have verified it, from the claim.
|
|
89
|
+
*
|
|
90
|
+
* A reboot drains, boots and starts the node. An update downloads the target, then restarts the
|
|
91
|
+
* node runtime on it: a drain and a start, no boot. A host upgrade runs the package upgrade and,
|
|
92
|
+
* with `reboot`, the reboot after it.
|
|
93
|
+
*/
|
|
94
|
+
export declare const nodeActionCompletionBudgetMs: (actionNameArg: TNodeActionName, rebootArg: boolean) => number;
|
|
95
|
+
/** Whether an action reboots the host, which is how Cloudly verifies it (`nodeActionVerification`). */
|
|
96
|
+
export declare const nodeActionReboots: (actionNameArg: TNodeActionName, rebootArg: boolean) => boolean;
|
|
97
|
+
/**
|
|
98
|
+
* What Cloudly accepts as evidence that a claimed action changed the node, by what the action does.
|
|
99
|
+
*
|
|
100
|
+
* - `new-boot`: an action that reboots is verified by a heartbeat, accepted after the claim, whose
|
|
101
|
+
* `boot.id` differs from the boot the claim stated.
|
|
102
|
+
* - `target-version`: an update is verified by a heartbeat, accepted after the claim, whose bundle
|
|
103
|
+
* version is the action's target version.
|
|
104
|
+
* - `upgrade-complete`: a host upgrade without a reboot is verified by Spark's `complete` and a
|
|
105
|
+
* heartbeat accepted after it; a heartbeat that states `boot.rebootRequired` fails the action
|
|
106
|
+
* `reboot-required` instead, because applying the upgrade needs a reboot the operator did not
|
|
107
|
+
* request. Cloudly never escalates to one.
|
|
108
|
+
*
|
|
109
|
+
* An action that restarts the node runtime (every action but a host upgrade without a reboot) is
|
|
110
|
+
* then only `succeeded` once the node's Pallet registered a runtime session after the claim and
|
|
111
|
+
* Cloudly accepted an observation of every replica slot on the node after that registration.
|
|
112
|
+
*/
|
|
113
|
+
export type TNodeActionVerification = 'new-boot' | 'target-version' | 'upgrade-complete';
|
|
114
|
+
export declare const nodeActionVerification: (actionNameArg: TNodeActionName, rebootArg: boolean) => TNodeActionVerification;
|
|
115
|
+
/** Whether the action restarts the node runtime and is verified by its return (see `nodeActionVerification`). */
|
|
116
|
+
export declare const nodeActionRestartsRuntime: (actionNameArg: TNodeActionName, rebootArg: boolean) => boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Why an action ended `failed`, each with the remedy an operator is told.
|
|
119
|
+
*
|
|
120
|
+
* The first group is stated by Spark (`TSparkNodeActionFailureCode`), the second by Cloudly.
|
|
121
|
+
*/
|
|
122
|
+
export declare const nodeActionFailureRemedies: Readonly<{
|
|
123
|
+
readonly 'download-failed': string;
|
|
124
|
+
readonly 'installer-mismatch': string;
|
|
125
|
+
readonly 'install-failed': string;
|
|
126
|
+
readonly 'drain-failed': string;
|
|
127
|
+
readonly 'reboot-failed': 'The host did not accept the reboot. Read the node\'s Spark journal.';
|
|
128
|
+
readonly 'host-upgrade-failed': string;
|
|
129
|
+
readonly 'executor-failed': 'Spark could not run the action. Read the node\'s Spark journal.';
|
|
130
|
+
readonly 'reboot-required': string;
|
|
131
|
+
readonly 'target-below-node': string;
|
|
132
|
+
}>;
|
|
133
|
+
export type TNodeActionFailureCode = keyof typeof nodeActionFailureRemedies;
|
|
134
|
+
/** The failure codes Spark may state with a `fail` transition. */
|
|
135
|
+
export declare const sparkNodeActionFailureCodes: readonly ["download-failed", "drain-failed", "executor-failed", "host-upgrade-failed", "install-failed", "installer-mismatch", "reboot-failed"];
|
|
136
|
+
export type TSparkNodeActionFailureCode = typeof sparkNodeActionFailureCodes[number];
|
|
137
|
+
/**
|
|
138
|
+
* Why an action ended `expired`.
|
|
139
|
+
*
|
|
140
|
+
* - `claim-window-passed`: no Spark claimed it within `nodeActionContract.claimWindowMs`.
|
|
141
|
+
* - `carrier-retired`: a Swarm-era record on a node that holds no Swarm-era session, which no
|
|
142
|
+
* carrier delivers: it states none of the frozen parameters and target the node runtime's
|
|
143
|
+
* lifecycle needs. Cloudly keeps the record.
|
|
144
|
+
*/
|
|
145
|
+
export type TNodeActionExpiryReason = 'claim-window-passed' | 'carrier-retired';
|
|
146
|
+
export interface INodeActionExpiry {
|
|
147
|
+
reason: TNodeActionExpiryReason;
|
|
148
|
+
at: number;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The Spark release an update moves a node to: the fleet Spark target as it stood when the
|
|
152
|
+
* operator requested the action, frozen into the action.
|
|
153
|
+
*/
|
|
154
|
+
export interface INodeActionTarget {
|
|
155
|
+
/** `MAJOR.MINOR.PATCH`, without `v`. */
|
|
156
|
+
version: string;
|
|
157
|
+
/** The SHA-256 of the release's `install.sh`, as `SHA256SUMS.txt` states it (lowercase hex). */
|
|
158
|
+
installerSha256: string;
|
|
159
|
+
/** The fleet target's revision this copy was taken from. */
|
|
160
|
+
targetRevision: number;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* What an operator confirmed when requesting a node runtime action, frozen into the action.
|
|
164
|
+
*
|
|
165
|
+
* The consent is the typed hostname and the digest of the impact preview the operator read:
|
|
166
|
+
* Cloudly refuses the request when either no longer matches what it reads now.
|
|
167
|
+
*/
|
|
168
|
+
export interface INodeActionConfirmation {
|
|
169
|
+
/** The hostname the operator typed; must equal the node's reported hostname. */
|
|
170
|
+
confirmedHostname: string;
|
|
171
|
+
/** `impactDigest` of the preview the operator confirmed (`computeNodeActionImpactDigest`). */
|
|
172
|
+
impactDigest: string;
|
|
173
|
+
/**
|
|
174
|
+
* Consent to restarting the Corestore the preview names on this node. Required `true` when the
|
|
175
|
+
* impact names one (`corestore`), and never assumed: a Corestore restart is never a default.
|
|
176
|
+
*/
|
|
177
|
+
corestoreRestart: boolean;
|
|
178
|
+
/** `systemUpgrade` only: reboot after the upgrade, as part of this one action. */
|
|
179
|
+
reboot: boolean;
|
|
180
|
+
/**
|
|
181
|
+
* `serveZoneServiceUpdate` only, and required there: the revision of the fleet Spark target the
|
|
182
|
+
* preview showed (`INodeActionPreview.target.targetRevision`). Cloudly refuses the update
|
|
183
|
+
* (`target-changed`) when the target it would freeze into the action has another revision, so the
|
|
184
|
+
* consent names the exact release the node is moved to. Absent for every other action.
|
|
185
|
+
*/
|
|
186
|
+
targetRevision?: number;
|
|
187
|
+
}
|
|
188
|
+
/** One service the action interrupts, as the impact preview names it. */
|
|
189
|
+
export interface INodeActionImpactService {
|
|
190
|
+
serviceId: string;
|
|
191
|
+
name: string;
|
|
192
|
+
/** Replica slots of this service on the node. */
|
|
193
|
+
slotsOnNode: number;
|
|
194
|
+
/** Replica slots of this service on other nodes; `0` means the service is down while the node is. */
|
|
195
|
+
slotsElsewhere: number;
|
|
196
|
+
/** The service is the cluster's designated ingress. */
|
|
197
|
+
clusterIngress: boolean;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* What a node action interrupts, read from Cloudly's database when it is previewed and again when
|
|
201
|
+
* it is requested.
|
|
202
|
+
*
|
|
203
|
+
* - `clusterNodeCount` 1: the node is its cluster, so the action is a full cluster outage.
|
|
204
|
+
* - `relayHost`: the node hosts its cluster's relay, so every inter-node connection of the cluster
|
|
205
|
+
* is interrupted while it is down. The relay never moves for an action.
|
|
206
|
+
* - `corestore`: the node runs a Corestore (`IClusterNode.data.corestore`), which restarts with it.
|
|
207
|
+
* - `services`: every service with a replica slot on the node, sorted by `serviceId`.
|
|
208
|
+
*/
|
|
209
|
+
export interface INodeActionImpact {
|
|
210
|
+
nodeId: string;
|
|
211
|
+
clusterId: string;
|
|
212
|
+
/** The hostname the node reports, which a confirmation types; `null` while it reports none. */
|
|
213
|
+
hostname: string | null;
|
|
214
|
+
clusterNodeCount: number;
|
|
215
|
+
relayHost: boolean;
|
|
216
|
+
corestore: boolean;
|
|
217
|
+
services: INodeActionImpactService[];
|
|
218
|
+
}
|
|
219
|
+
/** The SHA-256 (lowercase hex) of one impact's canonical JSON, which a confirmation names. */
|
|
220
|
+
export declare const computeNodeActionImpactDigest: (impactArg: INodeActionImpact) => Promise<string>;
|
|
221
|
+
/** When Spark claimed an action, and in which boot of the host. */
|
|
222
|
+
export interface INodeActionClaim {
|
|
223
|
+
claimedAt: number;
|
|
224
|
+
/** The host's `boot.id` the claim stated. */
|
|
225
|
+
bootId: string;
|
|
226
|
+
/** The Spark bundle version the node reported when it claimed. */
|
|
227
|
+
bundleVersion: string | null;
|
|
228
|
+
}
|
|
229
|
+
/** Why an action failed, and the remedy the operator was told (`nodeActionFailureRemedies`). */
|
|
230
|
+
export interface INodeActionFailure {
|
|
231
|
+
code: TNodeActionFailureCode;
|
|
232
|
+
remedy: string;
|
|
233
|
+
/** Spark's or Cloudly's own text, bounded by `nodeActionContract.maximumDetailLength`. */
|
|
234
|
+
detail: string | null;
|
|
235
|
+
}
|
|
236
|
+
/** Who moved an action into a state. */
|
|
237
|
+
export type TNodeActionActor = 'operator' | 'spark' | 'cloudly';
|
|
238
|
+
/** One state an action entered, in order. */
|
|
239
|
+
export interface INodeActionTransition {
|
|
240
|
+
status: TNodeActionStatus;
|
|
241
|
+
at: number;
|
|
242
|
+
actor: TNodeActionActor;
|
|
243
|
+
detail: string | null;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The node runtime's lifecycle of one action: what was confirmed and frozen at the request, and
|
|
247
|
+
* every state since. Absent on a Swarm-era record, which no carrier but the Swarm-era daemon's
|
|
248
|
+
* delivers.
|
|
249
|
+
*
|
|
250
|
+
* `revision` advances with every state change. Each change is one transaction that matches the
|
|
251
|
+
* action on its expected state and revision, so two writers never both move one action.
|
|
252
|
+
*/
|
|
253
|
+
export interface INodeActionLifecycle {
|
|
254
|
+
clusterId: string;
|
|
255
|
+
revision: number;
|
|
256
|
+
confirmation: INodeActionConfirmation;
|
|
257
|
+
impact: INodeActionImpact;
|
|
258
|
+
/** `serveZoneServiceUpdate` only. */
|
|
259
|
+
target: INodeActionTarget | null;
|
|
260
|
+
/** The claim window's end while `pending`; the completion deadline once claimed. */
|
|
261
|
+
deadlineAt: number;
|
|
262
|
+
claim: INodeActionClaim | null;
|
|
263
|
+
failure: INodeActionFailure | null;
|
|
264
|
+
timeline: INodeActionTransition[];
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Every reason Cloudly answers instead of admitting, previewing or cancelling a node action. Each
|
|
268
|
+
* travels by name as a `TypedResponseError`, and a refused request changes nothing.
|
|
269
|
+
*/
|
|
270
|
+
export declare const nodeActionRefusals: readonly ["node-missing", "node-action-undeliverable", "node-action-unsupported", "system-upgrade-unqualified", "node-action-open", "cluster-action-open", "spark-target-unset", "spark-target-below-node", "spark-target-held", "confirmation-invalid", "confirmation-hostname-mismatch", "impact-changed", "target-changed", "corestore-consent-required", "impact-unreadable", "node-action-missing", "node-action-not-cancellable", "node-action-revision-stale"];
|
|
271
|
+
export type TNodeActionRefusalCode = typeof nodeActionRefusals[number];
|
|
272
|
+
/** What requesting one action on one node would do, and whether Cloudly would admit it now. */
|
|
273
|
+
export interface INodeActionPreview {
|
|
274
|
+
nodeId: string;
|
|
275
|
+
actionName: TNodeActionName;
|
|
276
|
+
impact: INodeActionImpact;
|
|
277
|
+
/** What a confirmation names (`INodeActionConfirmation.impactDigest`). */
|
|
278
|
+
impactDigest: string;
|
|
279
|
+
/** The Spark bundle version the node reports; `null` while it reports none. */
|
|
280
|
+
nodeBundleVersion: string | null;
|
|
281
|
+
/** Whether the node's Spark states that the host needs a reboot; `null` while it states nothing. */
|
|
282
|
+
rebootRequired: boolean | null;
|
|
283
|
+
/** `serveZoneServiceUpdate` only: the fleet target the update moves the node to. */
|
|
284
|
+
target: INodeActionTarget | null;
|
|
285
|
+
/** Why a request would be refused now, in `nodeActionRefusals` order; empty when it would be admitted. */
|
|
286
|
+
refusals: TNodeActionRefusalCode[];
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Validates one confirmation for one action and names every reason it is refused.
|
|
290
|
+
*
|
|
291
|
+
* Shape only: an update must state `targetRevision`, every other action must not. Whether it
|
|
292
|
+
* matches the node, the impact and the fleet target Cloudly reads now is Cloudly's decision
|
|
293
|
+
* (`confirmation-hostname-mismatch`, `impact-changed`, `target-changed`, `corestore-consent-required`).
|
|
294
|
+
*/
|
|
295
|
+
export declare const validateNodeActionConfirmation: (valueArg: unknown, actionNameArg: TNodeActionName, pathArg?: string) => string[];
|
|
296
|
+
/** HTTPS POST body asking for the action Cloudly holds for this node. Never log, persist, or return nodeToken. */
|
|
297
|
+
export interface ISparkNodeActionOfferRequest {
|
|
298
|
+
nodeId: string;
|
|
299
|
+
nodeToken: string;
|
|
300
|
+
/** Read and negotiated before the node is authenticated; see `sparkNodeActionContracts`. */
|
|
301
|
+
protocol: IProtocolOffer;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* One action as Spark executes it: the node runtime's view of an open action.
|
|
305
|
+
*
|
|
306
|
+
* `status` is the action's state when Cloudly answered; `revision` is what Spark's next transition
|
|
307
|
+
* names. `claim` states the boot Spark claimed it in, so a Spark that restarted mid-action resumes
|
|
308
|
+
* by replaying the transition it had sent rather than claiming again.
|
|
309
|
+
*/
|
|
310
|
+
export interface ISparkNodeActionAssignment {
|
|
311
|
+
id: string;
|
|
312
|
+
actionName: TNodeActionName;
|
|
313
|
+
status: Extract<TNodeActionStatus, 'pending' | 'claimed' | 'executing' | 'draining' | 'rebooting' | 'verifying'>;
|
|
314
|
+
revision: number;
|
|
315
|
+
deadlineAt: number;
|
|
316
|
+
/** `systemUpgrade` only: reboot after the upgrade. */
|
|
317
|
+
reboot: boolean;
|
|
318
|
+
/** `serveZoneServiceUpdate` only: install exactly this release, after checking its installer's SHA-256. */
|
|
319
|
+
target: INodeActionTarget | null;
|
|
320
|
+
claim: {
|
|
321
|
+
bootId: string;
|
|
322
|
+
} | null;
|
|
323
|
+
}
|
|
324
|
+
/** What Cloudly answers the offer route: the node's one open action, or `null`. */
|
|
325
|
+
export interface ISparkNodeActionOffer {
|
|
326
|
+
nodeId: string;
|
|
327
|
+
action: ISparkNodeActionAssignment | null;
|
|
328
|
+
/** The controller's offer, so the node judges the answer it acts on. */
|
|
329
|
+
protocol: IProtocolOffer;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* One step Spark states about the action it executes.
|
|
333
|
+
*
|
|
334
|
+
* - `claim`: take a `pending` action, in the boot `bootId` names.
|
|
335
|
+
* - `phase`: the action entered a later phase (`sparkNodeActionPhaseOrder`). A reboot drains and
|
|
336
|
+
* then reboots; an update executes (downloads and installs) and then drains (restarts the node
|
|
337
|
+
* runtime on the new release); a host upgrade executes and, with `reboot`, drains and reboots.
|
|
338
|
+
* - `complete`: a host upgrade without a reboot finished; Cloudly verifies it from the next heartbeat.
|
|
339
|
+
* - `fail`: the action failed with one of `sparkNodeActionFailureCodes`.
|
|
340
|
+
*
|
|
341
|
+
* Every other ending is Cloudly's: it verifies an action from what the node reports
|
|
342
|
+
* (`nodeActionVerification`), and a deadline that passes ends one `unverified`.
|
|
343
|
+
*/
|
|
344
|
+
export type TSparkNodeActionTransition = {
|
|
345
|
+
kind: 'claim';
|
|
346
|
+
bootId: string;
|
|
347
|
+
} | {
|
|
348
|
+
kind: 'phase';
|
|
349
|
+
phase: TSparkNodeActionPhase;
|
|
350
|
+
} | {
|
|
351
|
+
kind: 'complete';
|
|
352
|
+
} | {
|
|
353
|
+
kind: 'fail';
|
|
354
|
+
code: TSparkNodeActionFailureCode;
|
|
355
|
+
detail: string;
|
|
356
|
+
};
|
|
357
|
+
/** HTTPS POST body stating one transition. Never log, persist, or return nodeToken. */
|
|
358
|
+
export interface ISparkNodeActionTransitionRequest {
|
|
359
|
+
nodeId: string;
|
|
360
|
+
nodeToken: string;
|
|
361
|
+
/** Read and negotiated before the node is authenticated; see `sparkNodeActionContracts`. */
|
|
362
|
+
protocol: IProtocolOffer;
|
|
363
|
+
actionId: string;
|
|
364
|
+
/** The revision of the assignment the transition was decided from. */
|
|
365
|
+
expectedRevision: number;
|
|
366
|
+
transition: TSparkNodeActionTransition;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Why Cloudly did not apply a transition.
|
|
370
|
+
*
|
|
371
|
+
* - `action-unknown`: no action of that id on this node.
|
|
372
|
+
* - `action-closed`: the action ended; `action` states it no more, Spark stops executing it.
|
|
373
|
+
* - `revision-stale`: the action moved since `expectedRevision`; `action` states where it stands.
|
|
374
|
+
* - `transition-invalid`: the action's state or kind does not admit this transition.
|
|
375
|
+
*/
|
|
376
|
+
export type TSparkNodeActionTransitionRefusal = 'action-unknown' | 'action-closed' | 'revision-stale' | 'transition-invalid';
|
|
377
|
+
/**
|
|
378
|
+
* What Cloudly answers a transition with.
|
|
379
|
+
*
|
|
380
|
+
* `replayed` answers a transition Cloudly had already applied at exactly that revision, so a
|
|
381
|
+
* Spark that lost the answer and sends it again is told it took effect. `action` is the action as
|
|
382
|
+
* it stands after the answer, `null` once it is no longer open.
|
|
383
|
+
*/
|
|
384
|
+
export type TSparkNodeActionTransitionAnswer = {
|
|
385
|
+
outcome: 'applied' | 'replayed';
|
|
386
|
+
action: ISparkNodeActionAssignment | null;
|
|
387
|
+
protocol: IProtocolOffer;
|
|
388
|
+
} | {
|
|
389
|
+
outcome: 'refused';
|
|
390
|
+
reason: TSparkNodeActionTransitionRefusal;
|
|
391
|
+
action: ISparkNodeActionAssignment | null;
|
|
392
|
+
protocol: IProtocolOffer;
|
|
393
|
+
};
|
|
394
|
+
/**
|
|
395
|
+
* The node runtime's action routes.
|
|
396
|
+
*
|
|
397
|
+
* Spark asks the offer route after each accepted heartbeat and states every step on the transition
|
|
398
|
+
* route. Both are authenticated with the node's Spark credential exactly like
|
|
399
|
+
* `sparkNodeHeartbeatContract` and judged in the same server order: the offer first, within
|
|
400
|
+
* `maxRequestBytes`, answered with `protocolRefusalStatus` and an `IProtocolRefusal` body on
|
|
401
|
+
* refusal, before the node is authenticated. The server's offer on these routes names, as its
|
|
402
|
+
* minimum, the release that introduced them, so a Spark below it is refused by name and is never
|
|
403
|
+
* handed an action.
|
|
404
|
+
*/
|
|
405
|
+
export declare const sparkNodeActionContracts: Readonly<{
|
|
406
|
+
readonly offer: Readonly<{
|
|
407
|
+
readonly endpoint: '/spark/nodes/action';
|
|
408
|
+
readonly maxRequestBytes: 1024;
|
|
409
|
+
readonly maxResponseBytes: 4096;
|
|
410
|
+
}>;
|
|
411
|
+
readonly transition: Readonly<{
|
|
412
|
+
readonly endpoint: '/spark/nodes/action/transition';
|
|
413
|
+
readonly maxRequestBytes: 2048;
|
|
414
|
+
readonly maxResponseBytes: 4096;
|
|
415
|
+
}>;
|
|
416
|
+
readonly protocolRefusalStatus: 422;
|
|
417
|
+
}>;
|
|
418
|
+
/** The kernel's `boot_id`: one lowercase UUID. */
|
|
419
|
+
export declare const isHostBootId: (valueArg: unknown) => valueArg is string;
|
|
420
|
+
/** One assignment, exactly its keys; detached and frozen. */
|
|
421
|
+
export declare const snapshotSparkNodeActionAssignment: (valueArg: unknown) => ISparkNodeActionAssignment;
|
|
422
|
+
export declare const snapshotSparkNodeActionOfferRequest: (valueArg: unknown) => ISparkNodeActionOfferRequest;
|
|
423
|
+
export declare const snapshotSparkNodeActionOffer: (valueArg: unknown) => ISparkNodeActionOffer;
|
|
424
|
+
export declare const snapshotSparkNodeActionTransitionRequest: (valueArg: unknown) => ISparkNodeActionTransitionRequest;
|
|
425
|
+
export declare const snapshotSparkNodeActionTransitionAnswer: (valueArg: unknown) => TSparkNodeActionTransitionAnswer;
|
|
426
|
+
/**
|
|
427
|
+
* Whether an open action admits one transition from Spark, judged from its state alone.
|
|
428
|
+
*
|
|
429
|
+
* `claim` takes a `pending` action. A `phase` moves strictly forward in `sparkNodeActionPhaseOrder`
|
|
430
|
+
* from `claimed` or an earlier phase, and only into a phase the action has: `rebooting` only for
|
|
431
|
+
* an action that reboots, `draining` for every action but a host upgrade without a reboot.
|
|
432
|
+
* `complete` ends the execution of a host upgrade without a reboot. `fail` ends any claimed action
|
|
433
|
+
* that Cloudly is not yet verifying.
|
|
434
|
+
*/
|
|
435
|
+
export declare const admitsSparkNodeActionTransition: (actionArg: Pick<ISparkNodeActionAssignment, 'actionName' | 'status' | 'reboot'>, transitionArg: TSparkNodeActionTransition) => boolean;
|
|
436
|
+
/** True when a value is a canonical list of action names: known names, strictly increasing, at least one. */
|
|
437
|
+
export declare const isNodeActionNameList: (valueArg: unknown) => valueArg is TNodeActionName[];
|
|
438
|
+
/** Sorts action names into canonical order, so a list built from a set compares byte for byte. */
|
|
439
|
+
export declare const sortNodeActionNames: (namesArg: Iterable<TNodeActionName>) => TNodeActionName[];
|