@hsuite/smart-engines-cli 1.5.1 → 1.6.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/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/deploy-manifest.d.ts +32 -0
- package/dist/_lib/deploy-manifest.d.ts.map +1 -1
- package/dist/_lib/deploy-manifest.js +71 -0
- package/dist/_lib/deploy-manifest.js.map +1 -1
- 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 +163 -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 +146 -15
- package/dist/commands/redeploy.js.map +1 -1
- package/dist/commands/status.d.ts +4 -3
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +15 -3
- package/dist/commands/status.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/update.d.ts +33 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +38 -2
- package/dist/commands/update.js.map +1 -1
- 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 +1 -1
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.probeClusterRuntimes = probeClusterRuntimes;
|
|
4
|
+
exports.assertClustersConverged = assertClustersConverged;
|
|
5
|
+
exports.waitForClusterConvergence = waitForClusterConvergence;
|
|
6
|
+
/**
|
|
7
|
+
* Per-cluster post-deploy convergence gate (#2787).
|
|
8
|
+
*
|
|
9
|
+
* ## Why this exists
|
|
10
|
+
*
|
|
11
|
+
* A deploy reaches exactly ONE cluster. `gateway.<domain>` is public-DNS
|
|
12
|
+
* round-robin over the three cluster ingresses, each fronting an INDEPENDENT
|
|
13
|
+
* smart-host + Mongo, and the runtime record is replicated to the peers
|
|
14
|
+
* asynchronously (the validator's `runtime.attached` fan-out). When that
|
|
15
|
+
* replication misses a cluster, nothing notices: the CLI polls the same
|
|
16
|
+
* round-robin name, gets a `RUNNING` from whichever cluster answers, and prints
|
|
17
|
+
* `✓ redeployed`.
|
|
18
|
+
*
|
|
19
|
+
* That is not hypothetical. On testnet 2026-08-22 (#2787) repeated
|
|
20
|
+
* `hsuite redeploy --tag v12` runs converged sn2 and sn3 and never sn1: sn1 kept
|
|
21
|
+
* a build-attestation pinning the PREVIOUS digest, its deployer refused the new
|
|
22
|
+
* image on every reconcile, and it served nothing for that app while the load
|
|
23
|
+
* balancer kept sending it a third of the traffic. Every run reported success.
|
|
24
|
+
*
|
|
25
|
+
* ## How it checks
|
|
26
|
+
*
|
|
27
|
+
* Round-robin polling cannot attribute an answer to a cluster, so we address the
|
|
28
|
+
* clusters directly: resolve the gateway's A-records and dial each ingress IP
|
|
29
|
+
* with TLS SNI + the HTTP `Host` header pinned to the gateway hostname — exactly
|
|
30
|
+
* the transport the registry fan-out already uses for the three Harbors
|
|
31
|
+
* ({@link defaultTransport}). Each cluster then:
|
|
32
|
+
* 1. names itself on the `@Public()` `GET /api/v3/cluster/health` route, and
|
|
33
|
+
* 2. issues its OWN session bearer (the XRPL web3 challenge/verify handshake —
|
|
34
|
+
* a session minted on one cluster is not valid on its peers), which is then
|
|
35
|
+
* used to read that cluster's own `…/deployment/apps/:appId/status`.
|
|
36
|
+
*
|
|
37
|
+
* The result is one observation per cluster, which {@link assertClustersConverged}
|
|
38
|
+
* turns into a hard error naming the cluster and both digests.
|
|
39
|
+
*
|
|
40
|
+
* ## What a PASS here does and does not mean (#2887)
|
|
41
|
+
*
|
|
42
|
+
* A pass means: every cluster the gateway advertises answered, and each one's
|
|
43
|
+
* runtime RECORD names the image ref this run deployed. It does NOT mean the
|
|
44
|
+
* running container's digest was verified — `runtime.image` is the desired-state
|
|
45
|
+
* ref the host attached, and no field on the status read path carries the
|
|
46
|
+
* `@sha256:` the deployer pins at apply time. Anything short of one answer per
|
|
47
|
+
* advertised cluster is an evidence gap and fails loudly rather than passing.
|
|
48
|
+
*
|
|
49
|
+
* Read-only: the probe issues auth handshakes and GETs, never a deploy.
|
|
50
|
+
*/
|
|
51
|
+
const registry_fanout_1 = require("./registry-fanout");
|
|
52
|
+
/** The host API is mounted behind this prefix on the gateway (matches `connectAndAuth`). */
|
|
53
|
+
const HOST_PATH_PREFIX = '/host';
|
|
54
|
+
/**
|
|
55
|
+
* Ask EVERY cluster behind the gateway what it has deployed for `appId`.
|
|
56
|
+
*
|
|
57
|
+
* One observation per resolved ingress IP, in endpoint order. A cluster that
|
|
58
|
+
* cannot be reached or authenticated yields `reachable: false` with `error` set
|
|
59
|
+
* — never a throw at the per-cluster level, so {@link assertClustersConverged}
|
|
60
|
+
* can weigh WHICH clusters gave evidence against how many were expected.
|
|
61
|
+
*
|
|
62
|
+
* #2887: a DNS failure is NOT one of those soft outcomes. It used to log
|
|
63
|
+
* `skipping the check` and return `[]`, and an empty observation list then fell
|
|
64
|
+
* straight through the old `observations.length <= 1` short-circuit in
|
|
65
|
+
* `assertClustersConverged` — so "I could not find the clusters" was reported
|
|
66
|
+
* as "the clusters agree". Not being able to enumerate the mesh is a total
|
|
67
|
+
* evidence gap; it throws, and {@link waitForClusterConvergence} gives it the
|
|
68
|
+
* same retry budget as any other transient fault before failing loudly.
|
|
69
|
+
*/
|
|
70
|
+
async function probeClusterRuntimes(opts) {
|
|
71
|
+
const host = (0, registry_fanout_1.registryHostOf)(opts.gateway);
|
|
72
|
+
const transport = opts.transport ?? (0, registry_fanout_1.defaultTransport)(host);
|
|
73
|
+
const timeoutMs = opts.timeoutMs ?? registry_fanout_1.DEFAULT_PROBE_TIMEOUT_MS;
|
|
74
|
+
const endpoints = opts.endpoints ?? (await (0, registry_fanout_1.resolveClusterEndpoints)(host, opts.resolve4));
|
|
75
|
+
return Promise.all(endpoints.map((ip) => probeOne(ip, { ...opts, transport, timeoutMs })));
|
|
76
|
+
}
|
|
77
|
+
async function probeOne(ip, opts) {
|
|
78
|
+
const send = (req) => (0, registry_fanout_1.withRequestTimeout)(opts.transport(req), opts.timeoutMs, `${req.method} ${req.path} on ${ip}`);
|
|
79
|
+
// 1. Who is this? Best-effort — a cluster that won't name itself is still
|
|
80
|
+
// worth probing, it just gets reported by IP.
|
|
81
|
+
let clusterId = null;
|
|
82
|
+
try {
|
|
83
|
+
const health = await send({ ip, method: 'GET', path: '/api/v3/cluster/health' });
|
|
84
|
+
if (health.status === 200) {
|
|
85
|
+
const body = parseJson(health);
|
|
86
|
+
const id = body?.clusterId;
|
|
87
|
+
if (typeof id === 'string' && id)
|
|
88
|
+
clusterId = id;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// fall through — the status read below decides reachability
|
|
93
|
+
}
|
|
94
|
+
// 2. This cluster's OWN session. Sessions are per-cluster: a bearer minted on
|
|
95
|
+
// a peer 401s here, which is exactly why the round-robin client cannot be
|
|
96
|
+
// reused for a per-cluster read.
|
|
97
|
+
// Cached across the retry polls of `waitForClusterConvergence`.
|
|
98
|
+
let token = opts.tokenCache?.get(ip);
|
|
99
|
+
// Only a REUSED bearer can have outlived its session; one minted a moment ago
|
|
100
|
+
// 401ing means something else is wrong and re-minting would just repeat it.
|
|
101
|
+
const tokenWasReused = token !== undefined;
|
|
102
|
+
if (!token) {
|
|
103
|
+
try {
|
|
104
|
+
token = await authenticateAgainst(ip, send, opts);
|
|
105
|
+
opts.tokenCache?.set(ip, token);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
return { ip, clusterId, reachable: false, error: `auth: ${err.message}` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 3. What does THIS cluster have deployed?
|
|
112
|
+
const statusPath = `${HOST_PATH_PREFIX}/api/v3/baas/deployment/apps/${encodeURIComponent(opts.appId)}/status`;
|
|
113
|
+
const readStatus = (bearer) => send({
|
|
114
|
+
ip,
|
|
115
|
+
method: 'GET',
|
|
116
|
+
path: statusPath,
|
|
117
|
+
headers: { Authorization: `Bearer ${bearer}`, Accept: 'application/json' },
|
|
118
|
+
});
|
|
119
|
+
try {
|
|
120
|
+
let res = await readStatus(token);
|
|
121
|
+
// A cached bearer can outlive its session. Re-mint once rather than let the
|
|
122
|
+
// 401 degrade into "unreachable" — an unreachable cluster is EXCLUDED from
|
|
123
|
+
// the divergence check, so a silently-expired token would weaken the gate.
|
|
124
|
+
if (res.status === 401 && tokenWasReused && opts.tokenCache) {
|
|
125
|
+
opts.tokenCache.delete(ip);
|
|
126
|
+
token = await authenticateAgainst(ip, send, opts);
|
|
127
|
+
opts.tokenCache.set(ip, token);
|
|
128
|
+
res = await readStatus(token);
|
|
129
|
+
}
|
|
130
|
+
if (res.status === 404) {
|
|
131
|
+
return { ip, clusterId, reachable: true, error: 'no runtime record on this cluster' };
|
|
132
|
+
}
|
|
133
|
+
if (res.status !== 200) {
|
|
134
|
+
return { ip, clusterId, reachable: false, error: `status read HTTP ${res.status}` };
|
|
135
|
+
}
|
|
136
|
+
const body = parseJson(res);
|
|
137
|
+
if (!body)
|
|
138
|
+
return { ip, clusterId, reachable: false, error: 'status read: malformed JSON' };
|
|
139
|
+
const obs = { ip, clusterId, reachable: true };
|
|
140
|
+
if (body.state !== undefined)
|
|
141
|
+
obs.state = body.state;
|
|
142
|
+
if (body.runtime?.image !== undefined)
|
|
143
|
+
obs.image = body.runtime.image;
|
|
144
|
+
if (body.runtime?.runtimeState !== undefined)
|
|
145
|
+
obs.runtimeState = body.runtime.runtimeState;
|
|
146
|
+
if (body.runtime?.lastError)
|
|
147
|
+
obs.lastError = body.runtime.lastError;
|
|
148
|
+
return obs;
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
return { ip, clusterId, reachable: false, error: err.message };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Run the web3 challenge/verify handshake against ONE cluster; returns its bearer. */
|
|
155
|
+
async function authenticateAgainst(ip, send, opts) {
|
|
156
|
+
const challengeRes = await send({
|
|
157
|
+
ip,
|
|
158
|
+
method: 'POST',
|
|
159
|
+
path: `${HOST_PATH_PREFIX}/api/v3/baas/auth/challenge`,
|
|
160
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
161
|
+
body: Buffer.from(JSON.stringify({
|
|
162
|
+
chain: opts.signer.chain,
|
|
163
|
+
walletAddress: opts.signer.walletAddress,
|
|
164
|
+
appId: opts.authAppId,
|
|
165
|
+
})),
|
|
166
|
+
});
|
|
167
|
+
const challenge = parseJson(challengeRes);
|
|
168
|
+
if (challengeRes.status >= 400 || !challenge?.challengeId || !challenge.message) {
|
|
169
|
+
throw new Error(`challenge HTTP ${challengeRes.status}`);
|
|
170
|
+
}
|
|
171
|
+
const signature = await opts.signer.signFn(challenge.message);
|
|
172
|
+
const verifyRes = await send({
|
|
173
|
+
ip,
|
|
174
|
+
method: 'POST',
|
|
175
|
+
path: `${HOST_PATH_PREFIX}/api/v3/baas/auth/verify`,
|
|
176
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
177
|
+
body: Buffer.from(JSON.stringify({
|
|
178
|
+
challengeId: challenge.challengeId,
|
|
179
|
+
signature,
|
|
180
|
+
publicKey: opts.signer.publicKey,
|
|
181
|
+
})),
|
|
182
|
+
});
|
|
183
|
+
const verified = parseJson(verifyRes);
|
|
184
|
+
if (verifyRes.status >= 400 || !verified?.token) {
|
|
185
|
+
throw new Error(`verify HTTP ${verifyRes.status}`);
|
|
186
|
+
}
|
|
187
|
+
return verified.token;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Throw unless EVERY cluster the gateway advertises answered, and every answer
|
|
191
|
+
* is the image this run deployed.
|
|
192
|
+
*
|
|
193
|
+
* Divergence is: a cluster pinned to a different image, a cluster whose runtime
|
|
194
|
+
* is `FAILED`, or a cluster with no runtime record at all. The error names each
|
|
195
|
+
* diverged cluster, what it is stuck on, and the digest this run attested — so
|
|
196
|
+
* the operator sees both sides of the mismatch instead of `✓ deployed`.
|
|
197
|
+
*
|
|
198
|
+
* ## Full evidence is required (#2887)
|
|
199
|
+
*
|
|
200
|
+
* This gate previously excluded unreachable clusters from the comparison and
|
|
201
|
+
* passed on whatever was left, short-circuiting entirely at
|
|
202
|
+
* `observations.length <= 1`. Both rules turned MISSING evidence into a pass:
|
|
203
|
+
* one matching answer out of three clusters read as "converged", and an empty
|
|
204
|
+
* probe (the gateway failing to resolve) read as "converged" too. Measured on
|
|
205
|
+
* testnet 2026-08-27: a deploy printed `✓ deployed` while `kubectl` showed sn1
|
|
206
|
+
* on `app:v33` and sn2 + sn3 still on `app:v32`.
|
|
207
|
+
*
|
|
208
|
+
* "I could not reach 2 of 3 clusters" and "all 3 clusters agree" are different
|
|
209
|
+
* sentences and only one of them is a pass. Transient faults are absorbed by
|
|
210
|
+
* the retry budget in {@link waitForClusterConvergence}, not by lowering the
|
|
211
|
+
* bar here; a mesh that is knowingly unreachable from the developer's network
|
|
212
|
+
* has `--skip-cluster-check`, which at least says so out loud.
|
|
213
|
+
*
|
|
214
|
+
* The one genuine no-op left: `expectedClusters <= 1`. A single-cluster / dev
|
|
215
|
+
* gateway has nothing to diverge from — the same rule the image fan-out
|
|
216
|
+
* applies at `endpoints.length <= 1`.
|
|
217
|
+
*
|
|
218
|
+
* ## What this can and cannot see
|
|
219
|
+
*
|
|
220
|
+
* `observation.image` is the per-cluster runtime RECORD (`runtime.image`,
|
|
221
|
+
* written by the host's `attachRuntime` call and read back through
|
|
222
|
+
* `GET …/deployment/apps/:appId/status`). It is a desired-state tag ref, not
|
|
223
|
+
* the digest of the container actually running: nothing on that read path
|
|
224
|
+
* surfaces the `@sha256:` the deployer pins at apply time. So this gate catches
|
|
225
|
+
* a cluster on the WRONG TAG or a `FAILED` reconcile, and cannot catch a
|
|
226
|
+
* cluster serving a different artifact under the same tag. Closing that needs
|
|
227
|
+
* the deployer's resolved digest surfaced in the runtime projection
|
|
228
|
+
* (smart-deployer → validator `runtime/status` → smart-host → SDK) — tracked
|
|
229
|
+
* separately; do not paper over it here with a comparison that has no digest
|
|
230
|
+
* to compare.
|
|
231
|
+
*/
|
|
232
|
+
function assertClustersConverged(args) {
|
|
233
|
+
if (args.expectedClusters <= 1)
|
|
234
|
+
return;
|
|
235
|
+
const reachable = args.observations.filter((o) => o.reachable);
|
|
236
|
+
// MISSING evidence is not evidence of convergence. Anything short of one
|
|
237
|
+
// answer per advertised cluster leaves clusters we know nothing about, and
|
|
238
|
+
// passing on the remainder is exactly the silent success this gate exists to
|
|
239
|
+
// remove.
|
|
240
|
+
if (reachable.length < args.expectedClusters) {
|
|
241
|
+
const silent = args.observations.filter((o) => !o.reachable);
|
|
242
|
+
const missing = args.expectedClusters - args.observations.length;
|
|
243
|
+
// `detail` is never empty here: entering this branch means
|
|
244
|
+
// `reachable.length < expectedClusters`, so either some observation is
|
|
245
|
+
// unreachable (→ a `silent` entry) or there are fewer observations than
|
|
246
|
+
// expected (→ `missing > 0`). No `|| 'no detail'` fallback, which would be
|
|
247
|
+
// a branch that cannot fire — the exact shape this PR removes elsewhere.
|
|
248
|
+
const detail = [
|
|
249
|
+
...silent.map((o) => `${safe(o.clusterId) ?? o.ip}=${safe(o.error) ?? 'no answer'}`),
|
|
250
|
+
...(missing > 0 ? [`${missing} endpoint(s) never probed`] : []),
|
|
251
|
+
];
|
|
252
|
+
throw new Error(`cluster convergence UNVERIFIED for ${args.appId}: only ${reachable.length} of ` +
|
|
253
|
+
`${args.expectedClusters} clusters answered the per-cluster status probe (` +
|
|
254
|
+
detail.join(', ') +
|
|
255
|
+
`). A cluster that gives no answer is one this deploy cannot vouch for — it may be ` +
|
|
256
|
+
`serving an older release while the load balancer keeps routing to it. Refusing to ` +
|
|
257
|
+
`report success without full evidence — re-run, or pass --skip-cluster-check if the ` +
|
|
258
|
+
`mesh is knowingly unreachable from here (#2787/#2887).`);
|
|
259
|
+
}
|
|
260
|
+
const diverged = reachable.filter((o) => o.image !== args.expectedImage || o.runtimeState === 'FAILED');
|
|
261
|
+
if (diverged.length === 0)
|
|
262
|
+
return;
|
|
263
|
+
const detail = diverged
|
|
264
|
+
.map((o) => {
|
|
265
|
+
const who = o.clusterId ? `${safe(o.clusterId)} (${o.ip})` : o.ip;
|
|
266
|
+
const bits = [
|
|
267
|
+
`image=${safe(o.image) ?? '<none>'}`,
|
|
268
|
+
`runtimeState=${safe(o.runtimeState) ?? '<none>'}`,
|
|
269
|
+
];
|
|
270
|
+
if (o.error)
|
|
271
|
+
bits.push(`probe=${safe(o.error)}`);
|
|
272
|
+
if (o.lastError)
|
|
273
|
+
bits.push(`lastError=${safe(o.lastError)}`);
|
|
274
|
+
return ` - ${who}: ${bits.join(' ')}`;
|
|
275
|
+
})
|
|
276
|
+
.join('\n');
|
|
277
|
+
throw new Error(`cluster divergence after deploying ${args.appId}: ${diverged.length} of ` +
|
|
278
|
+
`${reachable.length} reachable clusters did not converge on ${args.expectedImage} ` +
|
|
279
|
+
`(attested digest ${args.attestedDigest}).\n${detail}\n` +
|
|
280
|
+
' A cluster that keeps an older runtime record serves the OLD release (or nothing) ' +
|
|
281
|
+
'while the load balancer keeps routing to it, and it does not self-heal: the clusters ' +
|
|
282
|
+
'hold independent records and a deploy call reaches only the one the gateway picked. ' +
|
|
283
|
+
'Re-run the redeploy until this check passes — each run is another draw, and a run ' +
|
|
284
|
+
'that lands on the stuck cluster overwrites its record (a `build-attestation digest ' +
|
|
285
|
+
'MISMATCH` above is a STALE attestation on that cluster, not a bad image) (#2787).');
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Probe every cluster until they all run the deployed image, or the budget is
|
|
289
|
+
* exhausted — then throw ({@link assertClustersConverged}).
|
|
290
|
+
*
|
|
291
|
+
* The retry budget exists because peer replication is asynchronous: a cluster
|
|
292
|
+
* that simply hasn't caught up yet must not fail a healthy deploy. A cluster
|
|
293
|
+
* that is genuinely STUCK (a stale build-attestation, a crash-looping image)
|
|
294
|
+
* does not converge within any budget, which is the case this gate is for.
|
|
295
|
+
*
|
|
296
|
+
* #2887: the budget now covers the EVIDENCE-gathering failures too — a gateway
|
|
297
|
+
* that won't resolve, or clusters that won't answer. Those used to be reported
|
|
298
|
+
* as a pass; they are retried like any other transient fault and then fail
|
|
299
|
+
* loudly, because a check with no evidence has no verdict to give.
|
|
300
|
+
*
|
|
301
|
+
* The gateway is resolved ONCE and the answer threaded through every poll, so
|
|
302
|
+
* the expected cluster count is fixed for the whole wait. Re-resolving each
|
|
303
|
+
* poll would let a short round-robin answer shrink the expectation to match the
|
|
304
|
+
* clusters that happened to reply — the exact vacuity being removed.
|
|
305
|
+
*/
|
|
306
|
+
async function waitForClusterConvergence(opts) {
|
|
307
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
308
|
+
const interval = opts.intervalMs ?? 5000;
|
|
309
|
+
const maxPolls = opts.maxPolls ?? 6;
|
|
310
|
+
const log = opts.log ?? (() => undefined);
|
|
311
|
+
const host = (0, registry_fanout_1.registryHostOf)(opts.gateway);
|
|
312
|
+
const tokenCache = opts.tokenCache ?? new Map();
|
|
313
|
+
let endpoints = opts.endpoints ?? null;
|
|
314
|
+
for (let poll = 0;; poll++) {
|
|
315
|
+
const last = poll >= maxPolls - 1;
|
|
316
|
+
try {
|
|
317
|
+
if (!endpoints) {
|
|
318
|
+
const resolved = await (0, registry_fanout_1.resolveClusterEndpoints)(host, opts.resolve4);
|
|
319
|
+
// An EMPTY answer is a total evidence gap, not a single-cluster mesh.
|
|
320
|
+
// `expectedClusters: 0` would slip past the `<= 1` no-op below and turn
|
|
321
|
+
// the whole gate off — the one input that must not do that. Real
|
|
322
|
+
// `dns.resolve4` throws ENODATA rather than returning `[]`, but the
|
|
323
|
+
// resolver is an injectable seam and this is the gate's own off-switch.
|
|
324
|
+
if (resolved.length === 0) {
|
|
325
|
+
throw new Error(`cluster convergence UNVERIFIED for ${opts.appId}: ${host} returned no cluster ` +
|
|
326
|
+
'endpoints to probe, so there is no evidence any cluster runs ' +
|
|
327
|
+
`${opts.expectedImage}. Refusing to report success — re-run, or pass ` +
|
|
328
|
+
'--skip-cluster-check if the mesh is knowingly unreachable from here (#2887).');
|
|
329
|
+
}
|
|
330
|
+
endpoints = resolved;
|
|
331
|
+
}
|
|
332
|
+
const observations = await probeClusterRuntimes({ ...opts, endpoints, tokenCache });
|
|
333
|
+
assertClustersConverged({
|
|
334
|
+
appId: opts.appId,
|
|
335
|
+
expectedImage: opts.expectedImage,
|
|
336
|
+
attestedDigest: opts.attestedDigest,
|
|
337
|
+
expectedClusters: endpoints.length,
|
|
338
|
+
observations,
|
|
339
|
+
});
|
|
340
|
+
return observations;
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
if (last)
|
|
344
|
+
throw err;
|
|
345
|
+
// Name the actual reason: "not converged yet" and "could not gather the
|
|
346
|
+
// evidence" are different problems and the retry log used to claim the
|
|
347
|
+
// first for both. Through `safe()` — the reason can carry a cluster's own
|
|
348
|
+
// `error` text, and this line prints to the operator's terminal on every
|
|
349
|
+
// poll (same display-safety rule as the assertion messages below).
|
|
350
|
+
const reason = safe(err?.message?.split('\n')[0] ?? String(err));
|
|
351
|
+
log(` converge: ${opts.expectedImage} not confirmed on every cluster yet ` +
|
|
352
|
+
`(attempt ${poll + 1}/${maxPolls}: ${reason ?? 'unknown'}); re-checking in ${interval}ms`);
|
|
353
|
+
await sleep(interval);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Make a server-controlled string safe to print. `clusterId`, `image`,
|
|
359
|
+
* `runtimeState` and `lastError` all come verbatim from a cluster's JSON, and
|
|
360
|
+
* these error messages go straight to the operator's terminal via
|
|
361
|
+
* `console.error(chalk.red(...))` — so a compromised cluster would otherwise
|
|
362
|
+
* inject ANSI/control sequences into the console on every run against it.
|
|
363
|
+
* Strips C0/C1 control characters and bounds the length.
|
|
364
|
+
*/
|
|
365
|
+
function safe(v) {
|
|
366
|
+
if (v === undefined || v === null)
|
|
367
|
+
return undefined;
|
|
368
|
+
// eslint-disable-next-line no-control-regex
|
|
369
|
+
const stripped = v.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').trim();
|
|
370
|
+
return stripped.length > 400 ? `${stripped.slice(0, 400)}…` : stripped;
|
|
371
|
+
}
|
|
372
|
+
/** Parse a JSON response body; `null` when it isn't JSON. */
|
|
373
|
+
function parseJson(res) {
|
|
374
|
+
try {
|
|
375
|
+
return JSON.parse(res.body.toString('utf8'));
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
//# sourceMappingURL=cluster-convergence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cluster-convergence.js","sourceRoot":"","sources":["../../src/_lib/cluster-convergence.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
type BaasService = 'auth' | 'database' | 'storage' | 'functions' | 'messaging';
|
|
2
|
+
/**
|
|
3
|
+
* One external destination the smart-app declares it needs to reach (#2912).
|
|
4
|
+
*
|
|
5
|
+
* Tenant egress is default-deny: a pod that declares nothing can reach DNS and
|
|
6
|
+
* the platform BaaS endpoint, and nothing else. Declaring a destination is a
|
|
7
|
+
* REQUEST, not a grant — the cluster operator's destination allow-list decides
|
|
8
|
+
* whether it is honored, and a destination outside that list is dropped at
|
|
9
|
+
* reconcile time (visible in the tenant NetworkPolicy's annotations).
|
|
10
|
+
*/
|
|
11
|
+
export type ManifestEgress = {
|
|
12
|
+
/** Bare lowercase DNS hostname — no scheme, port, path or wildcard. */
|
|
13
|
+
host: string;
|
|
14
|
+
/** TCP port on `host`. Defaults to 443. */
|
|
15
|
+
port?: number;
|
|
16
|
+
};
|
|
2
17
|
/** Backend build + push + deploy inputs. REQUIRED — every smart-app has one. */
|
|
3
18
|
export type ManifestBackend = {
|
|
4
19
|
/** Backend listen port inside the customer container (1..65535, never 8080). */
|
|
@@ -26,6 +41,13 @@ export type ManifestBackend = {
|
|
|
26
41
|
cpu?: string;
|
|
27
42
|
memory?: string;
|
|
28
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* External hosts this app needs to reach (#2912). Optional; omitting it
|
|
46
|
+
* leaves whatever the app previously declared in place, so a redeploy from
|
|
47
|
+
* a manifest that predates this field never revokes working egress. To
|
|
48
|
+
* revoke, declare an explicit empty array.
|
|
49
|
+
*/
|
|
50
|
+
egress?: ManifestEgress[];
|
|
29
51
|
};
|
|
30
52
|
/**
|
|
31
53
|
* Frontend tar + upload inputs. OPTIONAL — present iff the smart-app ships a
|
|
@@ -65,5 +87,15 @@ export type Manifest = {
|
|
|
65
87
|
export declare function parseManifest(path: string): Manifest;
|
|
66
88
|
/** Validate an already-parsed manifest object. Shared by file + programmatic callers. */
|
|
67
89
|
export declare function validateManifest(raw: unknown): Manifest;
|
|
90
|
+
/**
|
|
91
|
+
* Resolve `manifest.backend.egress[]` into the wire shape the deploy API takes
|
|
92
|
+
* — every entry's port made explicit. Returns `undefined` when the manifest
|
|
93
|
+
* declares nothing, which the server reads as "leave the stored declaration
|
|
94
|
+
* alone" (as opposed to `[]`, which clears it).
|
|
95
|
+
*/
|
|
96
|
+
export declare function resolveEgress(manifest: Manifest): Array<{
|
|
97
|
+
host: string;
|
|
98
|
+
port: number;
|
|
99
|
+
}> | undefined;
|
|
68
100
|
export {};
|
|
69
101
|
//# sourceMappingURL=deploy-manifest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy-manifest.d.ts","sourceRoot":"","sources":["../../src/_lib/deploy-manifest.ts"],"names":[],"mappings":"AAmBA,KAAK,WAAW,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"deploy-manifest.d.ts","sourceRoot":"","sources":["../../src/_lib/deploy-manifest.ts"],"names":[],"mappings":"AAmBA,KAAK,WAAW,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;AAyB/E;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,gFAAgF;AAChF,MAAM,MAAM,eAAe,GAAG;IAC5B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,8EAA8E;IAC9E,MAAM,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;CAC3B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,eAAe,CAAC;IACzB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;CAC7B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAYpD;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CA8CvD;AA+CD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,QAAQ,GACjB,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,CAInD"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseManifest = parseManifest;
|
|
4
4
|
exports.validateManifest = validateManifest;
|
|
5
|
+
exports.resolveEgress = resolveEgress;
|
|
5
6
|
/**
|
|
6
7
|
* Shared smart-app deploy manifest schema + loader.
|
|
7
8
|
*
|
|
@@ -22,6 +23,22 @@ const fs_1 = require("fs");
|
|
|
22
23
|
const path_1 = require("path");
|
|
23
24
|
/** Lowest decentralization degree the host accepts (genesis cluster floor). */
|
|
24
25
|
const MIN_DEGREE = 3;
|
|
26
|
+
/**
|
|
27
|
+
* Default TCP port for a declared egress destination when the manifest entry
|
|
28
|
+
* omits one. HTTPS is what a smart-app dials in practice; anything else has to
|
|
29
|
+
* be stated.
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_EGRESS_PORT = 443;
|
|
32
|
+
/**
|
|
33
|
+
* Mirror of `DeployAppDto`'s hostname rule (smart-host,
|
|
34
|
+
* `apps/smart-host/src/deployment/dto/deploy-app.dto.ts`) — SSoT lives there;
|
|
35
|
+
* this copy exists so a typo fails at `hsuite deploy` with a message naming
|
|
36
|
+
* the manifest field, not as an opaque server 400 halfway through a build.
|
|
37
|
+
* Keep the two in step, exactly as `MIN_DEGREE` mirrors the DTO's degree floor.
|
|
38
|
+
*/
|
|
39
|
+
const EGRESS_HOST_REGEX = /^(?=.{1,253}$)(?!\d+\.\d+\.\d+\.\d+$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
|
40
|
+
/** Largest tier's destination cap — the DTO's schema-level ceiling. */
|
|
41
|
+
const MAX_EGRESS_ENTRIES = 50;
|
|
25
42
|
/**
|
|
26
43
|
* Parse + validate a deploy manifest from a JSON file.
|
|
27
44
|
*
|
|
@@ -82,6 +99,60 @@ function validateManifest(raw) {
|
|
|
82
99
|
if (frontend && !frontend.dir && !frontend.bundlePath) {
|
|
83
100
|
throw new Error('manifest.frontend requires `dir` or `bundlePath`');
|
|
84
101
|
}
|
|
102
|
+
validateEgress(backend.egress);
|
|
85
103
|
return raw;
|
|
86
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Validate `manifest.backend.egress[]` (#2912).
|
|
107
|
+
*
|
|
108
|
+
* Fails on the shapes a developer actually types — a URL, a `host:port`
|
|
109
|
+
* string, a wildcard, an IP literal — with a message that names the entry, so
|
|
110
|
+
* the mistake surfaces before the docker build rather than as a server 400
|
|
111
|
+
* after it. `undefined` is valid and means "don't touch the declaration".
|
|
112
|
+
*/
|
|
113
|
+
function validateEgress(value) {
|
|
114
|
+
if (value === undefined)
|
|
115
|
+
return;
|
|
116
|
+
if (!Array.isArray(value)) {
|
|
117
|
+
throw new Error('manifest.backend.egress must be an array when present');
|
|
118
|
+
}
|
|
119
|
+
if (value.length > MAX_EGRESS_ENTRIES) {
|
|
120
|
+
throw new Error(`manifest.backend.egress has ${value.length} entries; at most ${MAX_EGRESS_ENTRIES} ` +
|
|
121
|
+
`are accepted (your subscription tier may allow fewer)`);
|
|
122
|
+
}
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
for (const [i, entry] of value.entries()) {
|
|
125
|
+
const at = `manifest.backend.egress[${i}]`;
|
|
126
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
127
|
+
throw new Error(`${at} must be an object like { "host": "api.example.com", "port": 443 }`);
|
|
128
|
+
}
|
|
129
|
+
const { host, port } = entry;
|
|
130
|
+
if (typeof host !== 'string' || !EGRESS_HOST_REGEX.test(host)) {
|
|
131
|
+
throw new Error(`${at}.host must be a bare lowercase DNS hostname (e.g. "api.example.com") — ` +
|
|
132
|
+
`no scheme, port, path or wildcard; got ${JSON.stringify(host)}`);
|
|
133
|
+
}
|
|
134
|
+
if (port !== undefined) {
|
|
135
|
+
if (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
136
|
+
throw new Error(`${at}.port must be an integer 1-65535; got ${JSON.stringify(port)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const key = `${host}:${port ?? DEFAULT_EGRESS_PORT}`;
|
|
140
|
+
if (seen.has(key)) {
|
|
141
|
+
throw new Error(`${at} duplicates an earlier destination (${key})`);
|
|
142
|
+
}
|
|
143
|
+
seen.add(key);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Resolve `manifest.backend.egress[]` into the wire shape the deploy API takes
|
|
148
|
+
* — every entry's port made explicit. Returns `undefined` when the manifest
|
|
149
|
+
* declares nothing, which the server reads as "leave the stored declaration
|
|
150
|
+
* alone" (as opposed to `[]`, which clears it).
|
|
151
|
+
*/
|
|
152
|
+
function resolveEgress(manifest) {
|
|
153
|
+
const declared = manifest.backend.egress;
|
|
154
|
+
if (declared === undefined)
|
|
155
|
+
return undefined;
|
|
156
|
+
return declared.map((e) => ({ host: e.host, port: e.port ?? DEFAULT_EGRESS_PORT }));
|
|
157
|
+
}
|
|
87
158
|
//# sourceMappingURL=deploy-manifest.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy-manifest.js","sourceRoot":"","sources":["../../src/_lib/deploy-manifest.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"deploy-manifest.js","sourceRoot":"","sources":["../../src/_lib/deploy-manifest.ts"],"names":[],"mappings":"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"}
|
package/dist/_lib/docker.d.ts
CHANGED
|
@@ -46,37 +46,49 @@ export declare function dockerLogin(registry: RegistryCreds, spawnFn?: typeof sp
|
|
|
46
46
|
*/
|
|
47
47
|
export declare function parsePushDigest(output: string): string | null;
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
* `
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* in exactly one place.
|
|
49
|
+
* Parse the top-level manifest digest from `docker buildx imagetools inspect
|
|
50
|
+
* <ref>` output. `imagetools inspect` queries the REGISTRY (not the local
|
|
51
|
+
* daemon cache), so this is the authoritative `sha256:<64hex>` the registry
|
|
52
|
+
* serves for the tag — the value the attestation must bind.
|
|
54
53
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
54
|
+
* The default (non-`--raw`) report prints a top-level block:
|
|
55
|
+
* `Name: <ref>`
|
|
56
|
+
* `MediaType: application/vnd.docker.distribution.manifest.v2+json`
|
|
57
|
+
* `Digest: sha256:<64hex>`
|
|
58
|
+
* We match the FIRST `Digest:` line — the top-level manifest digest. (Per-arch
|
|
59
|
+
* child digests appear only under a later `Manifests:` block for a multi-arch
|
|
60
|
+
* index, which the single-arch amd64 deploy flow never produces.)
|
|
61
|
+
*
|
|
62
|
+
* @returns the lower-cased `sha256:<64hex>` digest, or `null` if absent.
|
|
59
63
|
*/
|
|
60
|
-
export declare function
|
|
64
|
+
export declare function parseImagetoolsDigest(output: string): string | null;
|
|
61
65
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
66
|
+
* Resolve the digest the REGISTRY currently serves for `<server>/<repository>:
|
|
67
|
+
* <tag>` via `docker buildx imagetools inspect`. Authoritative: it hits the
|
|
68
|
+
* registry, so — unlike a local `docker inspect ... .RepoDigests` read — a
|
|
69
|
+
* prior push of the same tag or a round-robin retry can never leave a STALE
|
|
70
|
+
* value here (#1679).
|
|
65
71
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
72
|
+
* @returns the `sha256:<64hex>` registry digest, or `null` if it could not be
|
|
73
|
+
* resolved (imagetools unavailable / registry unreachable / no digest line).
|
|
68
74
|
*/
|
|
69
|
-
export declare function
|
|
75
|
+
export declare function inspectRegistryDigest(registry: RegistryCreds, tag: string, spawnFn?: typeof spawn): Promise<string | null>;
|
|
70
76
|
/**
|
|
71
|
-
* Push the backend image and RECORD
|
|
77
|
+
* Push the backend image and RECORD the digest the registry serves for it.
|
|
72
78
|
*
|
|
73
|
-
* Runs `docker push <server>/<repository>:<tag>`,
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
79
|
+
* Runs `docker push <server>/<repository>:<tag>`, then resolves the pushed
|
|
80
|
+
* tag's digest from the REGISTRY via {@link inspectRegistryDigest} (NOT the
|
|
81
|
+
* local daemon cache). The `digest: sha256:...` line from the push summary,
|
|
82
|
+
* when present, is cross-checked against the registry digest and a MISMATCH is
|
|
83
|
+
* a hard error — that is the #1679 failure mode (a DNS-round-robin split /
|
|
84
|
+
* same-tag re-push surfacing a stale local digest that disagrees with what
|
|
85
|
+
* Harbor actually stored). Throws if the push fails, if the registry digest
|
|
86
|
+
* can't be resolved, or on a push/registry mismatch — a deploy must never
|
|
87
|
+
* proceed with an unverifiable or stale artifact reference.
|
|
77
88
|
*
|
|
78
|
-
* @returns the `sha256:<64hex>` manifest digest of the pushed tag.
|
|
79
|
-
* @throws if push exits non-zero,
|
|
89
|
+
* @returns the `sha256:<64hex>` registry manifest digest of the pushed tag.
|
|
90
|
+
* @throws if push exits non-zero, the registry digest is unresolvable, or the
|
|
91
|
+
* push-summary digest disagrees with the registry.
|
|
80
92
|
*/
|
|
81
93
|
export declare function dockerPush(registry: RegistryCreds, tag: string, spawnFn?: typeof spawn): Promise<string>;
|
|
82
94
|
//# sourceMappingURL=docker.d.ts.map
|