@hsuite/smart-engines-cli 1.5.0 → 1.5.2
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/README.md +5 -3
- package/dist/_lib/baas.d.ts +27 -4
- package/dist/_lib/baas.d.ts.map +1 -1
- package/dist/_lib/baas.js +76 -42
- package/dist/_lib/baas.js.map +1 -1
- package/dist/_lib/cluster-convergence.d.ts +214 -0
- package/dist/_lib/cluster-convergence.d.ts.map +1 -0
- package/dist/_lib/cluster-convergence.js +381 -0
- package/dist/_lib/cluster-convergence.js.map +1 -0
- package/dist/_lib/docker.d.ts +35 -23
- package/dist/_lib/docker.d.ts.map +1 -1
- package/dist/_lib/docker.js +58 -54
- package/dist/_lib/docker.js.map +1 -1
- package/dist/_lib/frontend-guard.d.ts +59 -0
- package/dist/_lib/frontend-guard.d.ts.map +1 -1
- package/dist/_lib/frontend-guard.js +148 -0
- package/dist/_lib/frontend-guard.js.map +1 -1
- package/dist/_lib/registry-fanout.d.ts +299 -9
- package/dist/_lib/registry-fanout.d.ts.map +1 -1
- package/dist/_lib/registry-fanout.js +557 -27
- package/dist/_lib/registry-fanout.js.map +1 -1
- package/dist/_lib/subscription-client.d.ts +14 -0
- package/dist/_lib/subscription-client.d.ts.map +1 -1
- package/dist/_lib/subscription-client.js +13 -0
- package/dist/_lib/subscription-client.js.map +1 -1
- package/dist/commands/deploy.d.ts +28 -4
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +157 -20
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/redeploy.d.ts +10 -0
- package/dist/commands/redeploy.d.ts.map +1 -1
- package/dist/commands/redeploy.js +140 -15
- package/dist/commands/redeploy.js.map +1 -1
- package/dist/commands/topup.d.ts +55 -0
- package/dist/commands/topup.d.ts.map +1 -0
- package/dist/commands/topup.js +180 -0
- package/dist/commands/topup.js.map +1 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +24 -1
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -278,9 +278,11 @@ authenticates as that id and fetches push creds for it (`pushCredentials`,
|
|
|
278
278
|
idempotent).
|
|
279
279
|
|
|
280
280
|
Flow: resolve `SUBSCRIPTION_APP_ID` → `connectAndAuth` → `pushCredentials` →
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
281
|
+
tar + `uploadFrontend` (frontend, when present — **before** the backend image
|
|
282
|
+
build, since the backend `Dockerfile` `COPY`s the frontend hook's gitignored
|
|
283
|
+
output, #2788) → docker build+push (backend) → **fan the image out to every
|
|
284
|
+
cluster Harbor** (verify + abort on any miss) → sign a build attestation →
|
|
285
|
+
`deploy()`. Writes `DEPLOYED_APP_ID` to the env file.
|
|
284
286
|
|
|
285
287
|
```bash
|
|
286
288
|
hsuite deploy --manifest ./my-app.json --env .env.local
|
package/dist/_lib/baas.d.ts
CHANGED
|
@@ -90,6 +90,26 @@ export type BuildAndPushOpts = {
|
|
|
90
90
|
runFn?: RunFn;
|
|
91
91
|
/** Push retry attempts (round-robin robot-auth can need a couple). Default 6. */
|
|
92
92
|
pushAttempts?: number;
|
|
93
|
+
/**
|
|
94
|
+
* SINGLE-ORIGIN registry host to aim the `tag` → `login` → `push` →
|
|
95
|
+
* `imagetools inspect` sequence at, instead of `registry.server` (#1679).
|
|
96
|
+
*
|
|
97
|
+
* `registry.server` is the round-robin name over three INDEPENDENT Harbors; a
|
|
98
|
+
* chunked blob upload that hops clusters mid-session dies (`blob upload
|
|
99
|
+
* invalid`), because the uploaded bytes live on the originating cluster's own
|
|
100
|
+
* RWO PVC. Resolve this with `resolvePushOrigin` (`_lib/registry-fanout.ts`),
|
|
101
|
+
* which asks the mesh for the name it publishes for itself.
|
|
102
|
+
*
|
|
103
|
+
* LOAD-BEARING: this pins the DOCKER SEQUENCE ONLY. `registry.server` is
|
|
104
|
+
* never rewritten — `resolveAttestationDigest` / `fanOutPushedImage` derive
|
|
105
|
+
* their per-cluster target list from it, and a collapsed single-origin value
|
|
106
|
+
* makes the fan-out resolve one endpoint and SILENTLY skip
|
|
107
|
+
* (`endpoints.length <= 1`), leaving two clusters unable to pull the image.
|
|
108
|
+
*
|
|
109
|
+
* Ignored when `skipPush` is set: nothing is uploaded, so the bytes are
|
|
110
|
+
* wherever a prior push left them and any cluster may answer the digest read.
|
|
111
|
+
*/
|
|
112
|
+
pushHost?: string;
|
|
93
113
|
};
|
|
94
114
|
/**
|
|
95
115
|
* The fully-qualified target image reference: `<server>/<repository>:<tag>`.
|
|
@@ -114,10 +134,13 @@ export declare function imageTarget(registry: RegistryCreds, tag: string): strin
|
|
|
114
134
|
* ref. The build attestation's `imageDigest` must carry this digest: the host
|
|
115
135
|
* binds it to the digest Harbor serves for the tag with strict equality, so a
|
|
116
136
|
* tag-form ref is rejected once `requireSignature` is enforced. The digest is
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
137
|
+
* resolved from the REGISTRY via `docker buildx imagetools inspect`
|
|
138
|
+
* ({@link inspectRegistryDigestOrThrow}) — the authoritative value Harbor
|
|
139
|
+
* serves — NOT the local daemon cache. The `docker push` summary digest
|
|
140
|
+
* ({@link parsePushDigest}), when present, is cross-checked against it and a
|
|
141
|
+
* mismatch is a hard error (#1679: a stale local digest / cross-cluster push
|
|
142
|
+
* split). Throws if no registry digest can be resolved or if it disagrees with
|
|
143
|
+
* the push summary — a deploy must never proceed with an unverifiable or stale
|
|
121
144
|
* artifact reference.
|
|
122
145
|
*
|
|
123
146
|
* NOTE: for a single-arch (linux/amd64, the platform standard) push the push
|
package/dist/_lib/baas.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baas.d.ts","sourceRoot":"","sources":["../../src/_lib/baas.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"baas.d.ts","sourceRoot":"","sources":["../../src/_lib/baas.ts"],"names":[],"mappings":"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"}
|
package/dist/_lib/baas.js
CHANGED
|
@@ -40,13 +40,16 @@ exports.tarFrontend = tarFrontend;
|
|
|
40
40
|
* (`--password-stdin`), never on argv. Both accept an injectable `runFn`
|
|
41
41
|
* (default `execFileSync`) so unit tests never invoke docker for real.
|
|
42
42
|
*
|
|
43
|
-
* {@link buildAndPush}
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
43
|
+
* {@link buildAndPush} records the pushed image's `sha256:<64hex>` registry
|
|
44
|
+
* manifest digest by resolving it from the REGISTRY via `docker buildx
|
|
45
|
+
* imagetools inspect` ({@link parseImagetoolsDigest}) — NOT from the local
|
|
46
|
+
* daemon cache, which a same-tag re-push or a DNS-round-robin retry can leave
|
|
47
|
+
* STALE (#1679). The `digest:` line the push summary carries (captured stdout,
|
|
48
|
+
* {@link parsePushDigest}) is cross-checked against the registry digest; a
|
|
49
|
+
* mismatch is a hard error. That digest is the value the build attestation's
|
|
50
|
+
* `imageDigest` must carry: the host binds it to the digest Harbor serves for
|
|
51
|
+
* the tag with STRICT equality, so a stale/tag-form ref would be rejected once
|
|
52
|
+
* `requireSignature` is enforced.
|
|
50
53
|
*/
|
|
51
54
|
const child_process_1 = require("child_process");
|
|
52
55
|
const smart_engines_sdk_1 = require("@hsuite/smart-engines-sdk");
|
|
@@ -110,10 +113,13 @@ function imageTarget(registry, tag) {
|
|
|
110
113
|
* ref. The build attestation's `imageDigest` must carry this digest: the host
|
|
111
114
|
* binds it to the digest Harbor serves for the tag with strict equality, so a
|
|
112
115
|
* tag-form ref is rejected once `requireSignature` is enforced. The digest is
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
116
|
+
* resolved from the REGISTRY via `docker buildx imagetools inspect`
|
|
117
|
+
* ({@link inspectRegistryDigestOrThrow}) — the authoritative value Harbor
|
|
118
|
+
* serves — NOT the local daemon cache. The `docker push` summary digest
|
|
119
|
+
* ({@link parsePushDigest}), when present, is cross-checked against it and a
|
|
120
|
+
* mismatch is a hard error (#1679: a stale local digest / cross-cluster push
|
|
121
|
+
* split). Throws if no registry digest can be resolved or if it disagrees with
|
|
122
|
+
* the push summary — a deploy must never proceed with an unverifiable or stale
|
|
117
123
|
* artifact reference.
|
|
118
124
|
*
|
|
119
125
|
* NOTE: for a single-arch (linux/amd64, the platform standard) push the push
|
|
@@ -181,41 +187,53 @@ function buildAndPush(opts) {
|
|
|
181
187
|
env: dockerEnv,
|
|
182
188
|
});
|
|
183
189
|
}
|
|
184
|
-
|
|
190
|
+
// #1679: aim the whole docker sequence at ONE origin when the caller resolved
|
|
191
|
+
// a pin. `opts.registry` itself is left alone — see `pushHost`'s docblock for
|
|
192
|
+
// why collapsing it breaks the downstream fan-out.
|
|
193
|
+
const pinnedHost = !opts.skipPush && opts.pushHost ? opts.pushHost : null;
|
|
194
|
+
const target = pinnedHost
|
|
195
|
+
? `${pinnedHost}/${opts.registry.repository}:${opts.tag}`
|
|
196
|
+
: imageTarget(opts.registry, opts.tag);
|
|
197
|
+
// docker keys stored credentials by registry HOST, so the login must name the
|
|
198
|
+
// same host the push dials or the pinned push goes out unauthenticated.
|
|
199
|
+
const loginHost = pinnedHost ?? opts.registry.server;
|
|
185
200
|
run('docker', ['tag', localTag, target], { stdio: 'inherit', env: dockerEnv });
|
|
186
201
|
if (opts.skipPush) {
|
|
187
202
|
// Nothing was pushed this run, but the image is asserted present at the
|
|
188
|
-
// registry —
|
|
189
|
-
//
|
|
190
|
-
return
|
|
203
|
+
// registry — resolve its digest from the registry (`buildx imagetools
|
|
204
|
+
// inspect`) so the attestation is bound to what Harbor actually serves.
|
|
205
|
+
return inspectRegistryDigestOrThrow(run, target, dockerEnv, null);
|
|
191
206
|
}
|
|
192
207
|
// CRITICAL: username is a discrete argv element (no shell, no `$` expansion);
|
|
193
208
|
// password via stdin. See the module header.
|
|
194
|
-
run('docker', ['login',
|
|
209
|
+
run('docker', ['login', loginHost, '-u', opts.registry.username, '--password-stdin'], { input: opts.registry.password, stdio: loginStdio, env: dockerEnv });
|
|
210
|
+
// Push with a retry budget (Harbor round-robin robot-auth occasionally 401s
|
|
211
|
+
// the first hit). Digest RESOLUTION happens AFTER a push succeeds — outside
|
|
212
|
+
// the retry loop — so a registry-verify failure or a digest mismatch surfaces
|
|
213
|
+
// as its own hard error rather than being masked as a push retry.
|
|
195
214
|
const attempts = opts.pushAttempts ?? 6;
|
|
196
215
|
let lastErr;
|
|
216
|
+
let pushOut = '';
|
|
217
|
+
let pushed = false;
|
|
197
218
|
for (let i = 1; i <= attempts; i++) {
|
|
198
219
|
try {
|
|
199
220
|
// Capture stdout (encoding 'utf8' → a string) so `parsePushDigest` can
|
|
200
|
-
// read the `… : digest: sha256:<64hex> size: …` summary line
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
206
|
-
const fromPush = (0, docker_1.parsePushDigest)(asStdout(pushOut));
|
|
207
|
-
if (fromPush)
|
|
208
|
-
return fromPush;
|
|
209
|
-
// Push succeeded but the summary carried no digest (older docker / buildx
|
|
210
|
-
// drivers). Recover it from `RepoDigests`.
|
|
211
|
-
return inspectDigestOrThrow(run, target, dockerEnv, opts.registry, opts.tag);
|
|
221
|
+
// read the `… : digest: sha256:<64hex> size: …` summary line for the
|
|
222
|
+
// registry cross-check.
|
|
223
|
+
pushOut = asStdout(run('docker', ['push', target], { stdio: captureStdio, encoding: 'utf8', env: dockerEnv }));
|
|
224
|
+
pushed = true;
|
|
225
|
+
break;
|
|
212
226
|
}
|
|
213
227
|
catch (err) {
|
|
214
228
|
lastErr = err;
|
|
215
|
-
// Harbor round-robin robot-auth occasionally 401s the first hit; retry.
|
|
216
229
|
}
|
|
217
230
|
}
|
|
218
|
-
|
|
231
|
+
if (!pushed) {
|
|
232
|
+
throw new Error(`docker push failed after ${attempts} attempts: ${lastErr?.message ?? 'unknown'}`);
|
|
233
|
+
}
|
|
234
|
+
// AUTHORITATIVE: bind the attestation to the digest the registry serves for
|
|
235
|
+
// the tag, cross-checked against the push summary (mismatch ⇒ throw, #1679).
|
|
236
|
+
return inspectRegistryDigestOrThrow(run, target, dockerEnv, (0, docker_1.parsePushDigest)(pushOut));
|
|
219
237
|
}
|
|
220
238
|
/** Coerce a `runFn` stdout return (string under `encoding:'utf8'`, else Buffer) to a string. */
|
|
221
239
|
function asStdout(out) {
|
|
@@ -226,23 +244,39 @@ function asStdout(out) {
|
|
|
226
244
|
return '';
|
|
227
245
|
}
|
|
228
246
|
/**
|
|
229
|
-
*
|
|
230
|
-
* `runFn` seam,
|
|
231
|
-
*
|
|
232
|
-
*
|
|
247
|
+
* Resolve the digest the REGISTRY serves for `<target>` via `docker buildx
|
|
248
|
+
* imagetools inspect` (the shared `runFn` seam), and — when a `pushDigest` from
|
|
249
|
+
* the push summary is supplied — assert they agree. `imagetools inspect` queries
|
|
250
|
+
* the registry, so unlike a local `docker inspect ... .RepoDigests` read it can
|
|
251
|
+
* never return a STALE same-tag digest (#1679).
|
|
252
|
+
*
|
|
253
|
+
* Throws if the registry digest can't be resolved, or if it disagrees with the
|
|
254
|
+
* push-summary digest — a deploy must never proceed with an unverifiable or
|
|
255
|
+
* stale artifact reference.
|
|
233
256
|
*/
|
|
234
|
-
function
|
|
235
|
-
|
|
257
|
+
function inspectRegistryDigestOrThrow(run, target, env, pushDigest) {
|
|
258
|
+
// Name the ref we ACTUALLY inspected. Under a #1679 push pin that is the
|
|
259
|
+
// single-origin target, NOT `registry.server`/<repo>:<tag> — a failure naming
|
|
260
|
+
// the round-robin host while the pinned host is what failed sends the reader
|
|
261
|
+
// to the wrong cluster.
|
|
262
|
+
const ref = target;
|
|
263
|
+
const inspectOut = asStdout(run('docker', ['buildx', 'imagetools', 'inspect', target], {
|
|
236
264
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
237
265
|
encoding: 'utf8',
|
|
238
266
|
env,
|
|
239
267
|
}));
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
268
|
+
const registryDigest = (0, docker_1.parseImagetoolsDigest)(inspectOut);
|
|
269
|
+
if (!registryDigest) {
|
|
270
|
+
throw new Error(`pushed ${ref} but could not resolve its registry manifest digest via ` +
|
|
271
|
+
'`docker buildx imagetools inspect` (registry unreachable or buildx unavailable). ' +
|
|
272
|
+
'Refusing to attest an unverifiable artifact reference.');
|
|
273
|
+
}
|
|
274
|
+
if (pushDigest && pushDigest !== registryDigest) {
|
|
275
|
+
throw new Error(`digest mismatch for ${ref}: push summary reported ${pushDigest} but the registry ` +
|
|
276
|
+
`serves ${registryDigest} (stale local digest / cross-cluster push split, #1679). ` +
|
|
277
|
+
'Refusing to attest a digest the registry does not serve.');
|
|
278
|
+
}
|
|
279
|
+
return registryDigest;
|
|
246
280
|
}
|
|
247
281
|
/**
|
|
248
282
|
* Tar a frontend `dir` into a plain `.tar` at `out` (the smart-host accepts a
|
package/dist/_lib/baas.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baas.js","sourceRoot":"","sources":["../../src/_lib/baas.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"baas.js","sourceRoot":"","sources":["../../src/_lib/baas.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-cluster post-deploy convergence gate (#2787).
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* A deploy reaches exactly ONE cluster. `gateway.<domain>` is public-DNS
|
|
7
|
+
* round-robin over the three cluster ingresses, each fronting an INDEPENDENT
|
|
8
|
+
* smart-host + Mongo, and the runtime record is replicated to the peers
|
|
9
|
+
* asynchronously (the validator's `runtime.attached` fan-out). When that
|
|
10
|
+
* replication misses a cluster, nothing notices: the CLI polls the same
|
|
11
|
+
* round-robin name, gets a `RUNNING` from whichever cluster answers, and prints
|
|
12
|
+
* `✓ redeployed`.
|
|
13
|
+
*
|
|
14
|
+
* That is not hypothetical. On testnet 2026-08-22 (#2787) repeated
|
|
15
|
+
* `hsuite redeploy --tag v12` runs converged sn2 and sn3 and never sn1: sn1 kept
|
|
16
|
+
* a build-attestation pinning the PREVIOUS digest, its deployer refused the new
|
|
17
|
+
* image on every reconcile, and it served nothing for that app while the load
|
|
18
|
+
* balancer kept sending it a third of the traffic. Every run reported success.
|
|
19
|
+
*
|
|
20
|
+
* ## How it checks
|
|
21
|
+
*
|
|
22
|
+
* Round-robin polling cannot attribute an answer to a cluster, so we address the
|
|
23
|
+
* clusters directly: resolve the gateway's A-records and dial each ingress IP
|
|
24
|
+
* with TLS SNI + the HTTP `Host` header pinned to the gateway hostname — exactly
|
|
25
|
+
* the transport the registry fan-out already uses for the three Harbors
|
|
26
|
+
* ({@link defaultTransport}). Each cluster then:
|
|
27
|
+
* 1. names itself on the `@Public()` `GET /api/v3/cluster/health` route, and
|
|
28
|
+
* 2. issues its OWN session bearer (the XRPL web3 challenge/verify handshake —
|
|
29
|
+
* a session minted on one cluster is not valid on its peers), which is then
|
|
30
|
+
* used to read that cluster's own `…/deployment/apps/:appId/status`.
|
|
31
|
+
*
|
|
32
|
+
* The result is one observation per cluster, which {@link assertClustersConverged}
|
|
33
|
+
* turns into a hard error naming the cluster and both digests.
|
|
34
|
+
*
|
|
35
|
+
* ## What a PASS here does and does not mean (#2887)
|
|
36
|
+
*
|
|
37
|
+
* A pass means: every cluster the gateway advertises answered, and each one's
|
|
38
|
+
* runtime RECORD names the image ref this run deployed. It does NOT mean the
|
|
39
|
+
* running container's digest was verified — `runtime.image` is the desired-state
|
|
40
|
+
* ref the host attached, and no field on the status read path carries the
|
|
41
|
+
* `@sha256:` the deployer pins at apply time. Anything short of one answer per
|
|
42
|
+
* advertised cluster is an evidence gap and fails loudly rather than passing.
|
|
43
|
+
*
|
|
44
|
+
* Read-only: the probe issues auth handshakes and GETs, never a deploy.
|
|
45
|
+
*/
|
|
46
|
+
import { type RegistryTransport } from './registry-fanout';
|
|
47
|
+
/** What ONE cluster reports about the app. */
|
|
48
|
+
export type ClusterRuntimeObservation = {
|
|
49
|
+
/** Cluster-ingress address dialled. */
|
|
50
|
+
ip: string;
|
|
51
|
+
/** Self-reported `CLUSTER_ID` (`sn1`…), or `null` when the cluster didn't name itself. */
|
|
52
|
+
clusterId: string | null;
|
|
53
|
+
/** Did this cluster answer the authenticated status read at all? */
|
|
54
|
+
reachable: boolean;
|
|
55
|
+
/** App lifecycle state (`ACTIVE` / `SUSPENDED` / …). */
|
|
56
|
+
state?: string;
|
|
57
|
+
/** Runtime state as THIS cluster's deployer sees it. */
|
|
58
|
+
runtimeState?: string;
|
|
59
|
+
/** Image ref THIS cluster has pinned — the field that exposes the divergence. */
|
|
60
|
+
image?: string;
|
|
61
|
+
/** This cluster's own reconcile error (carries the digest MISMATCH text). */
|
|
62
|
+
lastError?: string;
|
|
63
|
+
/** Probe-level failure (unreachable / auth failed / malformed answer). */
|
|
64
|
+
error?: string;
|
|
65
|
+
};
|
|
66
|
+
/** XRPL (or other chain) signer — same shape `buildSignerFromEnv` returns. */
|
|
67
|
+
export type ConvergenceSigner = {
|
|
68
|
+
chain: string;
|
|
69
|
+
walletAddress: string;
|
|
70
|
+
publicKey: string;
|
|
71
|
+
signFn: (message: string) => string | Promise<string>;
|
|
72
|
+
};
|
|
73
|
+
export type ProbeClusterRuntimesOpts = {
|
|
74
|
+
/** Public gateway URL (`https://gateway.<domain>`) — the round-robin name. */
|
|
75
|
+
gateway: string;
|
|
76
|
+
/** Management TARGET — the deployed app to read on every cluster. */
|
|
77
|
+
appId: string;
|
|
78
|
+
/** Auth IDENTITY — the developer's subscription appId (challenge context). */
|
|
79
|
+
authAppId: string;
|
|
80
|
+
/** Developer signer used for the per-cluster web3 handshake. */
|
|
81
|
+
signer: ConvergenceSigner;
|
|
82
|
+
/** Per-request timeout (ms). Default 10s. */
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
/** Test seam — the pinned HTTPS transport. Defaults to {@link defaultTransport}. */
|
|
85
|
+
transport?: RegistryTransport;
|
|
86
|
+
/** Test seam — DNS A-record resolver (defaults to `dns.promises.resolve4`). */
|
|
87
|
+
resolve4?: (h: string) => Promise<string[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Already-resolved cluster ingress IPs. {@link waitForClusterConvergence}
|
|
90
|
+
* resolves the gateway ONCE and threads the answer through every poll, so the
|
|
91
|
+
* expected cluster count cannot drift mid-wait (round-robin DNS can return a
|
|
92
|
+
* short answer, and a shrinking expectation would quietly re-open the
|
|
93
|
+
* partial-evidence hole this gate exists to close).
|
|
94
|
+
*/
|
|
95
|
+
endpoints?: string[];
|
|
96
|
+
/** Optional progress log sink (defaults to no-op). */
|
|
97
|
+
log?: (msg: string) => void;
|
|
98
|
+
/**
|
|
99
|
+
* Bearer cache keyed by cluster IP, shared across the retry polls of
|
|
100
|
+
* {@link waitForClusterConvergence} so a 6-poll wait mints 3 sessions, not 18.
|
|
101
|
+
*/
|
|
102
|
+
tokenCache?: Map<string, string>;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Ask EVERY cluster behind the gateway what it has deployed for `appId`.
|
|
106
|
+
*
|
|
107
|
+
* One observation per resolved ingress IP, in endpoint order. A cluster that
|
|
108
|
+
* cannot be reached or authenticated yields `reachable: false` with `error` set
|
|
109
|
+
* — never a throw at the per-cluster level, so {@link assertClustersConverged}
|
|
110
|
+
* can weigh WHICH clusters gave evidence against how many were expected.
|
|
111
|
+
*
|
|
112
|
+
* #2887: a DNS failure is NOT one of those soft outcomes. It used to log
|
|
113
|
+
* `skipping the check` and return `[]`, and an empty observation list then fell
|
|
114
|
+
* straight through the old `observations.length <= 1` short-circuit in
|
|
115
|
+
* `assertClustersConverged` — so "I could not find the clusters" was reported
|
|
116
|
+
* as "the clusters agree". Not being able to enumerate the mesh is a total
|
|
117
|
+
* evidence gap; it throws, and {@link waitForClusterConvergence} gives it the
|
|
118
|
+
* same retry budget as any other transient fault before failing loudly.
|
|
119
|
+
*/
|
|
120
|
+
export declare function probeClusterRuntimes(opts: ProbeClusterRuntimesOpts): Promise<ClusterRuntimeObservation[]>;
|
|
121
|
+
export type AssertConvergedArgs = {
|
|
122
|
+
appId: string;
|
|
123
|
+
/** The image ref this run deployed (`<server>/<repo>:<tag>`). */
|
|
124
|
+
expectedImage: string;
|
|
125
|
+
/** The digest this run attested — half of the pair an error message must carry. */
|
|
126
|
+
attestedDigest: string;
|
|
127
|
+
/**
|
|
128
|
+
* How many INDEPENDENT clusters the gateway advertises — the number of
|
|
129
|
+
* answers a converged verdict requires. Sourced from the gateway's A-records
|
|
130
|
+
* by {@link waitForClusterConvergence}, NOT from `observations.length`:
|
|
131
|
+
* deriving it from the answers we happened to get is what made the old gate
|
|
132
|
+
* vacuous (#2887). REQUIRED, deliberately — an optional field with a
|
|
133
|
+
* permissive default is how this hole would grow back.
|
|
134
|
+
*/
|
|
135
|
+
expectedClusters: number;
|
|
136
|
+
observations: ClusterRuntimeObservation[];
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Throw unless EVERY cluster the gateway advertises answered, and every answer
|
|
140
|
+
* is the image this run deployed.
|
|
141
|
+
*
|
|
142
|
+
* Divergence is: a cluster pinned to a different image, a cluster whose runtime
|
|
143
|
+
* is `FAILED`, or a cluster with no runtime record at all. The error names each
|
|
144
|
+
* diverged cluster, what it is stuck on, and the digest this run attested — so
|
|
145
|
+
* the operator sees both sides of the mismatch instead of `✓ deployed`.
|
|
146
|
+
*
|
|
147
|
+
* ## Full evidence is required (#2887)
|
|
148
|
+
*
|
|
149
|
+
* This gate previously excluded unreachable clusters from the comparison and
|
|
150
|
+
* passed on whatever was left, short-circuiting entirely at
|
|
151
|
+
* `observations.length <= 1`. Both rules turned MISSING evidence into a pass:
|
|
152
|
+
* one matching answer out of three clusters read as "converged", and an empty
|
|
153
|
+
* probe (the gateway failing to resolve) read as "converged" too. Measured on
|
|
154
|
+
* testnet 2026-08-27: a deploy printed `✓ deployed` while `kubectl` showed sn1
|
|
155
|
+
* on `app:v33` and sn2 + sn3 still on `app:v32`.
|
|
156
|
+
*
|
|
157
|
+
* "I could not reach 2 of 3 clusters" and "all 3 clusters agree" are different
|
|
158
|
+
* sentences and only one of them is a pass. Transient faults are absorbed by
|
|
159
|
+
* the retry budget in {@link waitForClusterConvergence}, not by lowering the
|
|
160
|
+
* bar here; a mesh that is knowingly unreachable from the developer's network
|
|
161
|
+
* has `--skip-cluster-check`, which at least says so out loud.
|
|
162
|
+
*
|
|
163
|
+
* The one genuine no-op left: `expectedClusters <= 1`. A single-cluster / dev
|
|
164
|
+
* gateway has nothing to diverge from — the same rule the image fan-out
|
|
165
|
+
* applies at `endpoints.length <= 1`.
|
|
166
|
+
*
|
|
167
|
+
* ## What this can and cannot see
|
|
168
|
+
*
|
|
169
|
+
* `observation.image` is the per-cluster runtime RECORD (`runtime.image`,
|
|
170
|
+
* written by the host's `attachRuntime` call and read back through
|
|
171
|
+
* `GET …/deployment/apps/:appId/status`). It is a desired-state tag ref, not
|
|
172
|
+
* the digest of the container actually running: nothing on that read path
|
|
173
|
+
* surfaces the `@sha256:` the deployer pins at apply time. So this gate catches
|
|
174
|
+
* a cluster on the WRONG TAG or a `FAILED` reconcile, and cannot catch a
|
|
175
|
+
* cluster serving a different artifact under the same tag. Closing that needs
|
|
176
|
+
* the deployer's resolved digest surfaced in the runtime projection
|
|
177
|
+
* (smart-deployer → validator `runtime/status` → smart-host → SDK) — tracked
|
|
178
|
+
* separately; do not paper over it here with a comparison that has no digest
|
|
179
|
+
* to compare.
|
|
180
|
+
*/
|
|
181
|
+
export declare function assertClustersConverged(args: AssertConvergedArgs): void;
|
|
182
|
+
export type WaitForClusterConvergenceOpts = ProbeClusterRuntimesOpts & {
|
|
183
|
+
/** The image ref this run deployed (`<server>/<repo>:<tag>`). */
|
|
184
|
+
expectedImage: string;
|
|
185
|
+
/** The digest this run attested. */
|
|
186
|
+
attestedDigest: string;
|
|
187
|
+
/** Delay between convergence polls (ms). Default 5s. */
|
|
188
|
+
intervalMs?: number;
|
|
189
|
+
/** Poll budget. Default 6 (~30s) — replication lag, not a wait-for-RUNNING. */
|
|
190
|
+
maxPolls?: number;
|
|
191
|
+
/** Injectable sleep for tests. */
|
|
192
|
+
sleep?: (ms: number) => Promise<void>;
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Probe every cluster until they all run the deployed image, or the budget is
|
|
196
|
+
* exhausted — then throw ({@link assertClustersConverged}).
|
|
197
|
+
*
|
|
198
|
+
* The retry budget exists because peer replication is asynchronous: a cluster
|
|
199
|
+
* that simply hasn't caught up yet must not fail a healthy deploy. A cluster
|
|
200
|
+
* that is genuinely STUCK (a stale build-attestation, a crash-looping image)
|
|
201
|
+
* does not converge within any budget, which is the case this gate is for.
|
|
202
|
+
*
|
|
203
|
+
* #2887: the budget now covers the EVIDENCE-gathering failures too — a gateway
|
|
204
|
+
* that won't resolve, or clusters that won't answer. Those used to be reported
|
|
205
|
+
* as a pass; they are retried like any other transient fault and then fail
|
|
206
|
+
* loudly, because a check with no evidence has no verdict to give.
|
|
207
|
+
*
|
|
208
|
+
* The gateway is resolved ONCE and the answer threaded through every poll, so
|
|
209
|
+
* the expected cluster count is fixed for the whole wait. Re-resolving each
|
|
210
|
+
* poll would let a short round-robin answer shrink the expectation to match the
|
|
211
|
+
* clusters that happened to reply — the exact vacuity being removed.
|
|
212
|
+
*/
|
|
213
|
+
export declare function waitForClusterConvergence(opts: WaitForClusterConvergenceOpts): Promise<ClusterRuntimeObservation[]>;
|
|
214
|
+
//# sourceMappingURL=cluster-convergence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cluster-convergence.d.ts","sourceRoot":"","sources":["../../src/_lib/cluster-convergence.ts"],"names":[],"mappings":"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"}
|