@caelo-cms/provisioning 0.11.9 → 0.11.10
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/dist/admin-domain.d.ts +10 -1
- package/dist/admin-domain.d.ts.map +1 -1
- package/dist/admin-domain.js +13 -3
- package/dist/admin-domain.js.map +1 -1
- package/dist/adopt-existing.d.ts +6 -1
- package/dist/adopt-existing.d.ts.map +1 -1
- package/dist/adopt-existing.js +86 -0
- package/dist/adopt-existing.js.map +1 -1
- package/dist/agent/command.d.ts +20 -0
- package/dist/agent/command.d.ts.map +1 -1
- package/dist/agent/command.js +32 -9
- package/dist/agent/command.js.map +1 -1
- package/dist/agent/run.d.ts +2 -0
- package/dist/agent/run.d.ts.map +1 -1
- package/dist/agent/run.js +5 -1
- package/dist/agent/run.js.map +1 -1
- package/dist/agent-db.js +44 -2
- package/dist/aws-task-roles.d.ts +4 -2
- package/dist/aws-task-roles.d.ts.map +1 -1
- package/dist/aws-task-roles.js +4 -1
- package/dist/aws-task-roles.js.map +1 -1
- package/dist/caddy.d.ts +21 -4
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +27 -3
- package/dist/caddy.js.map +1 -1
- package/dist/cli.js +12 -3
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-tls.d.ts +63 -0
- package/dist/cloudsql-tls.d.ts.map +1 -0
- package/dist/cloudsql-tls.js +289 -0
- package/dist/cloudsql-tls.js.map +1 -0
- package/dist/compose.d.ts +29 -6
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +95 -23
- package/dist/compose.js.map +1 -1
- package/dist/converge-checkpoint.d.ts +1 -0
- package/dist/converge-checkpoint.d.ts.map +1 -1
- package/dist/converge-checkpoint.js +8 -1
- package/dist/converge-checkpoint.js.map +1 -1
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +2 -11
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/deployed-release.d.ts +4 -1
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +18 -8
- package/dist/deployed-release.js.map +1 -1
- package/dist/gcp-names.d.ts +21 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +24 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts.map +1 -1
- package/dist/gcp-url-map.js +6 -2
- package/dist/gcp-url-map.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +24 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-state.d.ts +6 -0
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +17 -4
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +17 -8
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-auth-proxy-setup.d.ts +48 -0
- package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
- package/dist/mcp-auth-proxy-setup.js +280 -0
- package/dist/mcp-auth-proxy-setup.js.map +1 -0
- package/dist/release-manifest.d.ts +25 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +36 -3
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp.d.ts +55 -18
- package/dist/remote-mcp.d.ts.map +1 -1
- package/dist/remote-mcp.js +307 -77
- package/dist/remote-mcp.js.map +1 -1
- package/dist/runtime-secrets.d.ts +9 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +21 -10
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/seal-key-rotation.d.ts +7 -7
- package/dist/seal-key-rotation.d.ts.map +1 -1
- package/dist/seal-key-rotation.js +22 -22
- package/dist/seal-key-rotation.js.map +1 -1
- package/dist/self-hosted-db-tls.d.ts +37 -0
- package/dist/self-hosted-db-tls.d.ts.map +1 -0
- package/dist/self-hosted-db-tls.js +220 -0
- package/dist/self-hosted-db-tls.js.map +1 -0
- package/dist/self-hosted-release.d.ts +3 -1
- package/dist/self-hosted-release.d.ts.map +1 -1
- package/dist/self-hosted-release.js +33 -9
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +34 -0
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +52 -0
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-start.d.ts +0 -21
- package/dist/self-hosted-start.d.ts.map +1 -1
- package/dist/self-hosted-start.js +39 -1
- package/dist/self-hosted-start.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +15 -6
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +72 -23
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +170 -25
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +314 -37
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts +31 -0
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +71 -30
- package/dist/stack-converge.js.map +1 -1
- package/dist/upgrade/app-roll.d.ts +25 -4
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +63 -17
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +99 -12
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/health.d.ts +31 -7
- package/dist/upgrade/health.d.ts.map +1 -1
- package/dist/upgrade/health.js +41 -17
- package/dist/upgrade/health.js.map +1 -1
- package/dist/upgrade/image-forms.d.ts +1 -1
- package/dist/upgrade/image-forms.d.ts.map +1 -1
- package/dist/upgrade/image-forms.js +24 -7
- package/dist/upgrade/image-forms.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -3
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +13 -4
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +13 -3
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +34 -2
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +12 -0
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +49 -1
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts.map +1 -1
- package/dist/upgrader-iam-preflight.js +6 -1
- package/dist/upgrader-iam-preflight.js.map +1 -1
- package/dist/upgrader-job.d.ts +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +20 -14
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +1 -1
- package/dist/upgrader-records.d.ts.map +1 -1
- package/dist/upgrader-records.js +26 -7
- package/dist/upgrader-records.js.map +1 -1
- package/dist/upgrader-setup.d.ts.map +1 -1
- package/dist/upgrader-setup.js +10 -5
- package/dist/upgrader-setup.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +22 -7
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +43 -17
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-domain.test.ts +9 -8
- package/src/admin-domain.ts +13 -3
- package/src/admin-memory.test.ts +1 -0
- package/src/adopt-existing.test.ts +39 -0
- package/src/adopt-existing.ts +83 -1
- package/src/agent/command.test.ts +49 -1
- package/src/agent/command.ts +36 -9
- package/src/agent/run.test.ts +19 -0
- package/src/agent/run.ts +11 -2
- package/src/aws-task-roles.ts +5 -2
- package/src/caddy-redirects.docker.test.ts +2 -0
- package/src/caddy-redirects.test.ts +3 -0
- package/src/caddy-validate.test.ts +7 -1
- package/src/caddy.test.ts +39 -1
- package/src/caddy.ts +36 -4
- package/src/checkpoint-secrets.test.ts +14 -6
- package/src/cli.ts +12 -1
- package/src/cloudsql-tls.test.ts +225 -0
- package/src/cloudsql-tls.ts +352 -0
- package/src/compose.ts +125 -24
- package/src/converge-checkpoint.ts +8 -1
- package/src/converge-job-gcp.ts +2 -11
- package/src/database-tls-builders.test.ts +55 -0
- package/src/deployed-release.test.ts +104 -40
- package/src/deployed-release.ts +30 -9
- package/src/gateway-env-isolation.test.ts +10 -5
- package/src/gcp-names.ts +31 -0
- package/src/gcp-url-map.test.ts +6 -8
- package/src/gcp-url-map.ts +6 -2
- package/src/index.test.ts +7 -1
- package/src/index.ts +1 -0
- package/src/infra-contract.test.ts +5 -3
- package/src/infra-contract.ts +24 -1
- package/src/install-state.ts +26 -4
- package/src/lifecycle.ts +20 -11
- package/src/mcp-auth-proxy-setup.test.ts +305 -0
- package/src/mcp-auth-proxy-setup.ts +374 -0
- package/src/mcp-iap-stacks.test.ts +1 -1
- package/src/media-volume.test.ts +1 -1
- package/src/operator-access-stacks.test.ts +1 -1
- package/src/operator-access.test.ts +5 -4
- package/src/pool-budget-stacks.test.ts +1 -1
- package/src/release-manifest.test.ts +25 -0
- package/src/release-manifest.ts +59 -4
- package/src/remote-mcp-stack-config.test.ts +19 -9
- package/src/remote-mcp.test.ts +252 -57
- package/src/remote-mcp.ts +336 -77
- package/src/runtime-secrets.test.ts +20 -7
- package/src/runtime-secrets.ts +50 -9
- package/src/seal-key-rotation.test.ts +2 -2
- package/src/seal-key-rotation.ts +25 -25
- package/src/self-hosted-compose.test.ts +108 -5
- package/src/self-hosted-db-tls.test.ts +111 -0
- package/src/self-hosted-db-tls.ts +287 -0
- package/src/self-hosted-release-resolve.test.ts +71 -0
- package/src/self-hosted-release.ts +41 -11
- package/src/self-hosted-roles.test.ts +54 -0
- package/src/self-hosted-roles.ts +58 -0
- package/src/self-hosted-start.test.ts +34 -2
- package/src/self-hosted-start.ts +20 -2
- package/src/self-hosted-upgrade.test.ts +103 -14
- package/src/self-hosted-upgrade.ts +115 -29
- package/src/stack-contract-parity.test.ts +79 -12
- package/src/stack-contract.test.ts +169 -29
- package/src/stack-contract.ts +362 -42
- package/src/stack-converge-secrets.test.ts +26 -13
- package/src/stack-converge.test.ts +199 -26
- package/src/stack-converge.ts +92 -30
- package/src/state-store.test.ts +2 -1
- package/src/upgrade/app-roll.ts +97 -19
- package/src/upgrade/converge.ts +120 -11
- package/src/upgrade/health.test.ts +63 -25
- package/src/upgrade/health.ts +54 -22
- package/src/upgrade/image-forms.test.ts +34 -0
- package/src/upgrade/image-forms.ts +23 -8
- package/src/upgrade/release-images.test.ts +5 -1
- package/src/upgrade/release-images.ts +22 -6
- package/src/upgrade/upgrade.test.ts +353 -44
- package/src/upgrade/upgrade.ts +55 -6
- package/src/upgrader-gcp.ts +65 -3
- package/src/upgrader-iam-preflight.test.ts +19 -0
- package/src/upgrader-iam-preflight.ts +6 -0
- package/src/upgrader-job.test.ts +101 -27
- package/src/upgrader-job.ts +27 -14
- package/src/upgrader-records.test.ts +97 -12
- package/src/upgrader-records.ts +28 -11
- package/src/upgrader-setup.test.ts +2 -0
- package/src/upgrader-setup.ts +10 -4
- package/src/wizards/gcp-cost.test.ts +50 -0
- package/src/wizards/gcp-cost.ts +24 -7
- package/src/wizards/gcp.ts +55 -14
- package/stacks/aws/index.ts +32 -14
- package/stacks/azure/index.ts +57 -18
- package/stacks/gcp/index.ts +83 -23
- package/stacks/gcp-firebase/index.ts +162 -33
- package/stacks/self-hosted/README.md +2 -1
- package/stacks/self-hosted/index.ts +11 -1
package/src/upgrade/converge.ts
CHANGED
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
|
|
31
31
|
import { ensureAuditJob, resolveAdminImage } from "../audit-job-setup.js";
|
|
32
32
|
import { ensureCaptureJob } from "../capture-job-setup.js";
|
|
33
|
+
import {
|
|
34
|
+
dialsIpAddress,
|
|
35
|
+
ensureCloudSqlVerifyFull,
|
|
36
|
+
liveDatabaseUrls,
|
|
37
|
+
moveServicesToDnsHost,
|
|
38
|
+
} from "../cloudsql-tls.js";
|
|
33
39
|
import { ensureConvergeJob } from "../converge-job-setup.js";
|
|
34
40
|
import { findSqlInstance, planPublicRoleSwitch } from "../gateway-credentials.js";
|
|
35
41
|
import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
|
|
@@ -54,6 +60,8 @@ import {
|
|
|
54
60
|
CAPTURE_JOB_ENV_VAR,
|
|
55
61
|
type CloudRunEnvVar,
|
|
56
62
|
CONVERGE_JOB_ENV_VAR,
|
|
63
|
+
DATABASE_URL_VARS,
|
|
64
|
+
INTERIM_FORWARD_ORIGIN_ENV,
|
|
57
65
|
MCP_ENV_VAR,
|
|
58
66
|
OPERATOR_ACCESS_JOB_ENV_VAR,
|
|
59
67
|
remoteMcpIdentityEnv,
|
|
@@ -68,7 +76,7 @@ import {
|
|
|
68
76
|
import { ensureUpgraderJob, resolveUpgraderTarget } from "../upgrader-setup.js";
|
|
69
77
|
import { ensureUrlMapRedirectsAccess } from "../url-map-redirects-access.js";
|
|
70
78
|
import type { AppRollHooks, AppRollPlan, RollPlan, RollSlug } from "./app-roll.js";
|
|
71
|
-
import { lookupFailure } from "./gcp-lookup.js";
|
|
79
|
+
import { findServiceUrl, lookupFailure } from "./gcp-lookup.js";
|
|
72
80
|
import type { UpgradeReporter } from "./reporter.js";
|
|
73
81
|
|
|
74
82
|
/**
|
|
@@ -139,7 +147,11 @@ export function startConverge(
|
|
|
139
147
|
const { reporter: r, run } = deps;
|
|
140
148
|
const runDeps = { run, ...(deps.sleep ? { sleep: deps.sleep } : {}) };
|
|
141
149
|
const service = (slug: RollSlug) => plan.services.find((s) => s.slug === slug) as RollPlan;
|
|
142
|
-
const services = {
|
|
150
|
+
const services = {
|
|
151
|
+
admin: service("admin").serviceName,
|
|
152
|
+
gateway: service("gateway").serviceName,
|
|
153
|
+
"mcp-auth-proxy": service("mcp-auth-proxy").serviceName,
|
|
154
|
+
};
|
|
143
155
|
const target = {
|
|
144
156
|
provider: install.provider,
|
|
145
157
|
projectId: install.projectId,
|
|
@@ -371,33 +383,50 @@ export function startConverge(
|
|
|
371
383
|
}
|
|
372
384
|
|
|
373
385
|
async function remoteMcpEdge(): Promise<void> {
|
|
374
|
-
// #806 — on `gcp` the load balancer routes the remote-MCP
|
|
375
|
-
//
|
|
376
|
-
// /api/mcp/* (remote-mcp-edge.ts). Only remote
|
|
377
|
-
// failure warns.
|
|
386
|
+
// #806 / #1328 — on `gcp` the load balancer routes the remote-MCP paths
|
|
387
|
+
// to the MCP-Auth-Proxy's own backend, whose Cloud Armor policy lets
|
|
388
|
+
// agent traffic through on /api/mcp/* (remote-mcp-edge.ts). Only remote
|
|
389
|
+
// MCP depends on it, so a failure warns. The gateway being rolled in
|
|
390
|
+
// this run no longer answers those paths, so until this succeeds remote
|
|
391
|
+
// MCP is down on the site's domain — said below.
|
|
378
392
|
if (install.provider !== "gcp") return;
|
|
379
|
-
const s = r.step("Ensuring the
|
|
393
|
+
const s = r.step("Ensuring the MCP-Auth-Proxy's load-balancer route and Cloud Armor rule...");
|
|
380
394
|
const edge = await ensureRemoteMcpEdge(
|
|
381
395
|
{
|
|
382
396
|
provider: install.provider,
|
|
383
397
|
projectId: install.projectId,
|
|
398
|
+
region: install.region,
|
|
384
399
|
env: install.env,
|
|
385
400
|
domain: install.domain,
|
|
401
|
+
mcpAuthProxyImageRef: service("mcp-auth-proxy").imageRef,
|
|
386
402
|
},
|
|
387
403
|
{ ...runDeps, ...(deps.http ? { http: deps.http } : {}) },
|
|
388
404
|
);
|
|
389
405
|
if (edge.ok) {
|
|
390
406
|
for (const line of edge.done) r.info(` ${line}`);
|
|
391
|
-
s.done("Remote-MCP load-balancer
|
|
407
|
+
s.done("Remote-MCP load-balancer route ready");
|
|
392
408
|
return;
|
|
393
409
|
}
|
|
394
|
-
s.warned(`Remote-MCP load-balancer
|
|
410
|
+
s.warned(`Remote-MCP load-balancer route not set up: ${edge.error}`);
|
|
395
411
|
r.warn(
|
|
396
412
|
"The upgrade continues; remote MCP (Settings → MCP → Remote) cannot be reached through the site's domain until this succeeds. Re-run upgrade once the cause is fixed.",
|
|
397
413
|
);
|
|
398
414
|
incomplete = true;
|
|
399
415
|
}
|
|
400
416
|
|
|
417
|
+
async function interimForwardEnv(): Promise<CloudRunEnvVar[]> {
|
|
418
|
+
// #1328 — on gcp-firebase the proxy is the service the current live
|
|
419
|
+
// release's Firebase rewrite and the staging domain mapping still
|
|
420
|
+
// point at; it forwards that traffic to the data gateway, whose URL is
|
|
421
|
+
// its origin (stack-contract.ts INTERIM_FORWARD_ORIGIN_ENV). Without it
|
|
422
|
+
// the proxy answers the live site's /api/** with 404 until the next
|
|
423
|
+
// Publish live, and the staging host with 404 — so this aborts.
|
|
424
|
+
if (install.provider !== "gcp-firebase") return [];
|
|
425
|
+
const url = await findServiceUrl(run, install.projectId, install.region, services.gateway);
|
|
426
|
+
if (!url) throw new Error(`could not read the URL of ${services.gateway}`);
|
|
427
|
+
return [{ name: INTERIM_FORWARD_ORIGIN_ENV, value: new URL(url).origin }];
|
|
428
|
+
}
|
|
429
|
+
|
|
401
430
|
async function remoteMcpIdentity(): Promise<Record<RollSlug, CloudRunEnvVar[]> | null> {
|
|
402
431
|
// #806 — CAELO_ADMIN_URL / CAELO_MCP_PUBLIC_URL / CAELO_IAP_AUDIENCE
|
|
403
432
|
// name identities the env contract cannot (stack-contract.ts
|
|
@@ -459,6 +488,71 @@ export function startConverge(
|
|
|
459
488
|
return name ? { kind: "backend-services", service: name } : null;
|
|
460
489
|
}
|
|
461
490
|
|
|
491
|
+
/** #1338 — the host the database TLS step moved the services onto. */
|
|
492
|
+
let databaseHost: string | null = null;
|
|
493
|
+
|
|
494
|
+
async function databaseTls(): Promise<boolean> {
|
|
495
|
+
// #1338 — verify-full TLS needs the shared server CA, the instance's
|
|
496
|
+
// DNS name resolving in the VPC, and the services dialling that name
|
|
497
|
+
// (cloudsql-tls.ts). Before migrations: the migration job reads its host
|
|
498
|
+
// from the admin's live URL, and the new images refuse anything less
|
|
499
|
+
// than verify-full. A failure aborts — the new revisions could not
|
|
500
|
+
// reach the database.
|
|
501
|
+
const s = r.step("Setting up verify-full TLS to the database (shared CA, private DNS name)...");
|
|
502
|
+
const liveUrls = plan.services.map((p) => ({
|
|
503
|
+
serviceName: p.serviceName,
|
|
504
|
+
liveUrls: liveDatabaseUrls(p.liveEnv, DATABASE_URL_VARS[p.slug]),
|
|
505
|
+
}));
|
|
506
|
+
// Only this step moves the services off the instance's IP, and only
|
|
507
|
+
// after the instance, its certificate and the DNS name are in shape —
|
|
508
|
+
// so services that dial a host name already went through it.
|
|
509
|
+
const onIp = liveUrls.some((u) => Object.values(u.liveUrls).some((v) => dialsIpAddress(v)));
|
|
510
|
+
if (!onIp) {
|
|
511
|
+
s.done("Database TLS ready: the services already dial the instance's DNS name");
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
const lookup = await findSqlInstance(install, { run });
|
|
515
|
+
if (lookup.status !== "found") {
|
|
516
|
+
s.failed(
|
|
517
|
+
`${lookupFailure(lookup, "Cloud SQL instance", install.credentials)}. Aborting upgrade.`,
|
|
518
|
+
);
|
|
519
|
+
r.warn(
|
|
520
|
+
"No traffic was shifted and no migrations ran. Re-run upgrade once the instance is found.",
|
|
521
|
+
);
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
const tlsDeps = { ...runDeps, ...(deps.http ? { http: deps.http } : {}) };
|
|
525
|
+
const tls = await ensureCloudSqlVerifyFull(
|
|
526
|
+
{ projectId: install.projectId, env: install.env, sqlInstance: lookup.name },
|
|
527
|
+
tlsDeps,
|
|
528
|
+
);
|
|
529
|
+
for (const line of tls.done) r.info(` ${line}`);
|
|
530
|
+
if (!tls.ok) {
|
|
531
|
+
s.failed(`Database TLS could not be set up: ${tls.error}. Aborting upgrade.`);
|
|
532
|
+
r.warn("No traffic was shifted and no migrations ran. Fix the above and re-run.");
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
const moved = await moveServicesToDnsHost(
|
|
536
|
+
{ projectId: install.projectId, region: install.region },
|
|
537
|
+
liveUrls,
|
|
538
|
+
tls.host,
|
|
539
|
+
tlsDeps,
|
|
540
|
+
);
|
|
541
|
+
for (const line of moved.done) r.info(` ${line}`);
|
|
542
|
+
if (!moved.ok) {
|
|
543
|
+
s.failed(
|
|
544
|
+
`The services could not be moved onto ${tls.host}: ${moved.error}. Aborting upgrade.`,
|
|
545
|
+
);
|
|
546
|
+
r.warn("No migrations ran. Fix the above and re-run.");
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
databaseHost = tls.host;
|
|
550
|
+
s.done(
|
|
551
|
+
`Database TLS ready: the services dial ${tls.host} (verify-full from the new revisions)`,
|
|
552
|
+
);
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
|
|
462
556
|
async function databaseLogins(): Promise<boolean> {
|
|
463
557
|
// #613 — public_role moves onto its own password right before the
|
|
464
558
|
// gateway's roll. The gateway has no other database login (#635).
|
|
@@ -628,6 +722,7 @@ export function startConverge(
|
|
|
628
722
|
if (!(await runtimeIdentities())) return false;
|
|
629
723
|
if (!(await stackInvariants())) return false;
|
|
630
724
|
if (!(await publicRoleIsolation())) return false;
|
|
725
|
+
if (!(await databaseTls())) return false;
|
|
631
726
|
await mcpAccess();
|
|
632
727
|
await urlMapRedirectsAccess();
|
|
633
728
|
await auditJob();
|
|
@@ -642,11 +737,25 @@ export function startConverge(
|
|
|
642
737
|
await upgraderJob();
|
|
643
738
|
await convergeJob();
|
|
644
739
|
await operatorAccess();
|
|
645
|
-
const
|
|
740
|
+
const identity = await remoteMcpIdentity();
|
|
741
|
+
let forward: CloudRunEnvVar[];
|
|
742
|
+
try {
|
|
743
|
+
forward = await interimForwardEnv();
|
|
744
|
+
} catch (e) {
|
|
745
|
+
r.error(
|
|
746
|
+
`The MCP-Auth-Proxy cannot be pointed at the data gateway: ${e instanceof Error ? e.message : String(e)}. Nothing was rolled; re-run upgrade once it is fixed.`,
|
|
747
|
+
);
|
|
748
|
+
return { ok: false };
|
|
749
|
+
}
|
|
750
|
+
const extraEnv: Partial<Record<RollSlug, CloudRunEnvVar[]>> = {
|
|
751
|
+
...(identity ?? {}),
|
|
752
|
+
"mcp-auth-proxy": [...(identity?.["mcp-auth-proxy"] ?? []), ...forward],
|
|
753
|
+
};
|
|
646
754
|
return {
|
|
647
755
|
ok: true,
|
|
648
756
|
leaveUntouched: [...leaveUntouched],
|
|
649
|
-
|
|
757
|
+
extraEnv,
|
|
758
|
+
...(databaseHost ? { databaseHost } : {}),
|
|
650
759
|
};
|
|
651
760
|
},
|
|
652
761
|
async beforeRoll(slug) {
|
|
@@ -6,7 +6,7 @@ import type { HttpFetch } from "../runtime-secrets.js";
|
|
|
6
6
|
import {
|
|
7
7
|
createIapHealthProbe,
|
|
8
8
|
evaluateAdminDeepHealth,
|
|
9
|
-
|
|
9
|
+
evaluatePublicServiceDeepHealth,
|
|
10
10
|
type HealthTarget,
|
|
11
11
|
healthOrigins,
|
|
12
12
|
IAM_PROPAGATION_WINDOW_MS,
|
|
@@ -72,27 +72,29 @@ describe("evaluateAdminDeepHealth", () => {
|
|
|
72
72
|
});
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
describe("
|
|
76
|
-
it("accepts a healthy gateway
|
|
77
|
-
expect(
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
describe("evaluatePublicServiceDeepHealth", () => {
|
|
76
|
+
it("accepts a healthy gateway; its version is not public any more (#1326)", () => {
|
|
77
|
+
expect(evaluatePublicServiceDeepHealth("gateway", 200, JSON.stringify({ ok: true }))).toEqual({
|
|
78
|
+
ok: true,
|
|
79
|
+
detail: "gateway healthy",
|
|
80
|
+
});
|
|
80
81
|
});
|
|
81
82
|
|
|
82
|
-
it("
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
it("ignores a version an older gateway still reports", () => {
|
|
84
|
+
expect(
|
|
85
|
+
evaluatePublicServiceDeepHealth(
|
|
86
|
+
"gateway",
|
|
87
|
+
200,
|
|
88
|
+
JSON.stringify({ ok: true, version: "0.10.0" }),
|
|
89
|
+
).ok,
|
|
90
|
+
).toBe(true);
|
|
89
91
|
});
|
|
90
92
|
|
|
91
93
|
it("refuses a gateway that can't reach its database", () => {
|
|
92
|
-
const r =
|
|
94
|
+
const r = evaluatePublicServiceDeepHealth(
|
|
95
|
+
"gateway",
|
|
93
96
|
503,
|
|
94
|
-
JSON.stringify({ ok: false,
|
|
95
|
-
"0.11.0",
|
|
97
|
+
JSON.stringify({ ok: false, error: { kind: "DbUnreachable" } }),
|
|
96
98
|
);
|
|
97
99
|
expect(r).toEqual({ ok: false, error: "gateway reports unhealthy (HTTP 503, DbUnreachable)" });
|
|
98
100
|
});
|
|
@@ -103,7 +105,8 @@ const FIREBASE: HealthTarget = {
|
|
|
103
105
|
projectId: "acme",
|
|
104
106
|
domain: "acme.com",
|
|
105
107
|
adminServiceUrl: "https://caelo-production-admin-aaa-xyz.a.run.app",
|
|
106
|
-
gatewayServiceUrl: "https://caelo-production-gateway-
|
|
108
|
+
gatewayServiceUrl: "https://caelo-production-data-gateway-ddd-xyz.a.run.app",
|
|
109
|
+
mcpAuthProxyServiceUrl: "https://caelo-production-gateway-bbb-xyz.a.run.app",
|
|
107
110
|
};
|
|
108
111
|
|
|
109
112
|
describe("healthOrigins", () => {
|
|
@@ -111,13 +114,15 @@ describe("healthOrigins", () => {
|
|
|
111
114
|
expect(healthOrigins({ ...FIREBASE, provider: "gcp" })).toEqual({
|
|
112
115
|
admin: "https://admin.acme.com",
|
|
113
116
|
gateway: "https://acme.com",
|
|
117
|
+
mcpAuthProxy: "https://acme.com",
|
|
114
118
|
});
|
|
115
119
|
});
|
|
116
120
|
|
|
117
121
|
it("uses the run.app URLs on gcp-firebase", () => {
|
|
118
122
|
expect(healthOrigins(FIREBASE)).toEqual({
|
|
119
123
|
admin: "https://caelo-production-admin-aaa-xyz.a.run.app",
|
|
120
|
-
gateway: "https://caelo-production-gateway-
|
|
124
|
+
gateway: "https://caelo-production-data-gateway-ddd-xyz.a.run.app",
|
|
125
|
+
mcpAuthProxy: "https://caelo-production-gateway-bbb-xyz.a.run.app",
|
|
121
126
|
});
|
|
122
127
|
});
|
|
123
128
|
});
|
|
@@ -130,8 +135,12 @@ describe("createIapHealthProbe", () => {
|
|
|
130
135
|
exitCode: 0,
|
|
131
136
|
});
|
|
132
137
|
|
|
133
|
-
/** A fake network: signJwt + the
|
|
134
|
-
function network(answers: {
|
|
138
|
+
/** A fake network: signJwt + the three health endpoints (the proxy answers healthy unless given). */
|
|
139
|
+
function network(answers: {
|
|
140
|
+
admin: () => [number, string];
|
|
141
|
+
gateway: () => [number, string];
|
|
142
|
+
proxy?: () => [number, string];
|
|
143
|
+
}) {
|
|
135
144
|
const requests: { url: string; headers: Record<string, string>; body?: string }[] = [];
|
|
136
145
|
const http: HttpFetch = async (url, init) => {
|
|
137
146
|
requests.push({ url, headers: init.headers, ...(init.body ? { body: init.body } : {}) });
|
|
@@ -139,7 +148,9 @@ describe("createIapHealthProbe", () => {
|
|
|
139
148
|
? [200, JSON.stringify({ signedJwt: "signed.jwt" })]
|
|
140
149
|
: url.includes("admin")
|
|
141
150
|
? answers.admin()
|
|
142
|
-
:
|
|
151
|
+
: url.endsWith("/api/mcp/health")
|
|
152
|
+
? (answers.proxy ?? (() => [200, JSON.stringify({ ok: true, version: "0.11.0" })]))()
|
|
153
|
+
: answers.gateway();
|
|
143
154
|
return { ok: status < 400, status, text: async () => text };
|
|
144
155
|
};
|
|
145
156
|
return { http, requests };
|
|
@@ -148,7 +159,7 @@ describe("createIapHealthProbe", () => {
|
|
|
148
159
|
it("signs an IAP credential for the admin origin and sends it to the admin only", async () => {
|
|
149
160
|
const net = network({
|
|
150
161
|
admin: () => [200, adminBody()],
|
|
151
|
-
gateway: () => [200, JSON.stringify({ ok: true
|
|
162
|
+
gateway: () => [200, JSON.stringify({ ok: true })],
|
|
152
163
|
});
|
|
153
164
|
const probe = createIapHealthProbe(FIREBASE, {
|
|
154
165
|
run: gcloudOk,
|
|
@@ -160,7 +171,7 @@ describe("createIapHealthProbe", () => {
|
|
|
160
171
|
expect(await probe.deep({ version: "0.11.0" })).toEqual({
|
|
161
172
|
ok: true,
|
|
162
173
|
detail:
|
|
163
|
-
"admin 0.11.0, migrations at 0262_tool_approval_branch.sql / 0040_x.sql; gateway
|
|
174
|
+
"admin 0.11.0, migrations at 0262_tool_approval_branch.sql / 0040_x.sql; gateway healthy; MCP-Auth-Proxy healthy",
|
|
164
175
|
});
|
|
165
176
|
const sign = net.requests[0];
|
|
166
177
|
expect(sign?.url).toBe(
|
|
@@ -180,9 +191,36 @@ describe("createIapHealthProbe", () => {
|
|
|
180
191
|
expect(admin?.headers.Authorization).toBe("Bearer signed.jwt");
|
|
181
192
|
const gateway = net.requests.find((r) => r.url.endsWith("/api/_caelo/health?deep=1"));
|
|
182
193
|
expect(gateway?.url).toBe(
|
|
183
|
-
"https://caelo-production-gateway-
|
|
194
|
+
"https://caelo-production-data-gateway-ddd-xyz.a.run.app/api/_caelo/health?deep=1",
|
|
184
195
|
);
|
|
185
196
|
expect(gateway?.headers.Authorization).toBeUndefined();
|
|
197
|
+
// #1328 — the proxy, on the service that was the gateway, by version.
|
|
198
|
+
const proxy = net.requests.find((r) => r.url.endsWith("/api/mcp/health"));
|
|
199
|
+
expect(proxy?.url).toBe("https://caelo-production-gateway-bbb-xyz.a.run.app/api/mcp/health");
|
|
200
|
+
expect(proxy?.headers.Authorization).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("#1328 — fails the deep check while the MCP-Auth-Proxy is unhealthy or does not answer", async () => {
|
|
204
|
+
const deepWith = (proxy: () => [number, string]) =>
|
|
205
|
+
createIapHealthProbe(FIREBASE, {
|
|
206
|
+
run: gcloudOk,
|
|
207
|
+
http: network({
|
|
208
|
+
admin: () => [200, adminBody()],
|
|
209
|
+
gateway: () => [200, JSON.stringify({ ok: true, version: "0.11.0" })],
|
|
210
|
+
proxy,
|
|
211
|
+
}).http,
|
|
212
|
+
sleep: async () => {},
|
|
213
|
+
accessJustChanged: false,
|
|
214
|
+
attempts: 1,
|
|
215
|
+
}).deep({ version: "0.11.0" });
|
|
216
|
+
expect(await deepWith(() => [503, JSON.stringify({ ok: false })])).toEqual({
|
|
217
|
+
ok: false,
|
|
218
|
+
error: "MCP-Auth-Proxy reports unhealthy (HTTP 503, no reason)",
|
|
219
|
+
});
|
|
220
|
+
expect(await deepWith(() => [404, "<html>Not Found</html>"])).toEqual({
|
|
221
|
+
ok: false,
|
|
222
|
+
error: "MCP-Auth-Proxy answered HTTP 404 without JSON",
|
|
223
|
+
});
|
|
186
224
|
});
|
|
187
225
|
|
|
188
226
|
it("retries while the new revisions warm up, then gives the last error", async () => {
|
|
@@ -194,7 +232,7 @@ describe("createIapHealthProbe", () => {
|
|
|
194
232
|
? [503, adminBody({ ok: false, migrations: migrations(1) })]
|
|
195
233
|
: [200, adminBody()];
|
|
196
234
|
},
|
|
197
|
-
gateway: () => [200, JSON.stringify({ ok: true
|
|
235
|
+
gateway: () => [200, JSON.stringify({ ok: true })],
|
|
198
236
|
});
|
|
199
237
|
const probe = createIapHealthProbe(FIREBASE, {
|
|
200
238
|
run: gcloudOk,
|
package/src/upgrade/health.ts
CHANGED
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
* - admin `GET /_caelo/health?deep=1` → `{ ok, version, migrations }`:
|
|
12
12
|
* both databases reachable as the expected roles, and every
|
|
13
13
|
* migration the image ships recorded as applied.
|
|
14
|
-
* - gateway `GET /api/_caelo/health?deep=1` → `{ ok
|
|
15
|
-
* database
|
|
14
|
+
* - gateway `GET /api/_caelo/health?deep=1` → `{ ok }` (no version since
|
|
15
|
+
* #1326): its database login works. Under `/api/` because that is the only
|
|
16
16
|
* path the gcp load balancer routes to the gateway.
|
|
17
|
+
* - MCP-Auth-Proxy `GET /api/mcp/health` → `{ ok }` (#1328, no version
|
|
18
|
+
* either): it has no database, so it answers once it runs; under
|
|
19
|
+
* `/api/mcp/`, the prefix the gcp load balancer routes to it.
|
|
17
20
|
*
|
|
18
21
|
* Both answer with no secrets and no raw database errors. The admin sits
|
|
19
22
|
* behind IAP, so the probe signs an IAP credential as the install's MCP
|
|
@@ -134,29 +137,41 @@ export function evaluateAdminDeepHealth(
|
|
|
134
137
|
};
|
|
135
138
|
}
|
|
136
139
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Judge a public service's deep health answer: the gateway's, or the
|
|
142
|
+
* MCP-Auth-Proxy's (#1328). Since #1326 the public services report no
|
|
143
|
+
* release version (a public version string names the flaws to try), so
|
|
144
|
+
* this proves only that the service answers healthy — for the gateway,
|
|
145
|
+
* that its database login works. Which release it runs the roll itself
|
|
146
|
+
* establishes: `update --image` with the pinned digest succeeded and
|
|
147
|
+
* traffic moved to that revision (app-roll.ts), or the roll already failed.
|
|
148
|
+
*
|
|
149
|
+
* @param service how the errors name it (`gateway`, `MCP-Auth-Proxy`)
|
|
150
|
+
*/
|
|
151
|
+
export function evaluatePublicServiceDeepHealth(
|
|
152
|
+
service: string,
|
|
139
153
|
status: number,
|
|
140
154
|
text: string,
|
|
141
|
-
expectedVersion: string,
|
|
142
155
|
): HealthOutcome {
|
|
143
|
-
const body = parseJson<{ ok?: boolean;
|
|
144
|
-
if (!body) return { ok: false, error:
|
|
156
|
+
const body = parseJson<{ ok?: boolean; error?: { kind?: string } }>(text);
|
|
157
|
+
if (!body) return { ok: false, error: `${service} answered HTTP ${status} without JSON` };
|
|
145
158
|
if (status !== 200 || body.ok !== true) {
|
|
146
159
|
return {
|
|
147
160
|
ok: false,
|
|
148
|
-
error:
|
|
161
|
+
error: `${service} reports unhealthy (HTTP ${status}, ${body.error?.kind ?? "no reason"})`,
|
|
149
162
|
};
|
|
150
163
|
}
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
ok: false,
|
|
154
|
-
error: `gateway runs ${body.version ?? "an unknown version"}, the admin runs ${expectedVersion}`,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
return { ok: true, detail: `gateway ${body.version}` };
|
|
164
|
+
return { ok: true, detail: `${service} healthy` };
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
/**
|
|
168
|
+
* The MCP-Auth-Proxy's health path (apps/mcp-auth-proxy server.ts). Under
|
|
169
|
+
* `/api/mcp/` because on gcp that is the prefix the load balancer routes to
|
|
170
|
+
* the proxy; it answers before any other routing, also while remote MCP is
|
|
171
|
+
* off.
|
|
172
|
+
*/
|
|
173
|
+
export const MCP_AUTH_PROXY_HEALTH_PATH = "/api/mcp/health";
|
|
174
|
+
|
|
160
175
|
/** Where the probe reaches the install. */
|
|
161
176
|
export interface HealthTarget {
|
|
162
177
|
readonly provider: "gcp" | "gcp-firebase";
|
|
@@ -166,20 +181,32 @@ export interface HealthTarget {
|
|
|
166
181
|
readonly adminServiceUrl: string;
|
|
167
182
|
/** The gateway service's `run.app` URL (public on gcp-firebase). */
|
|
168
183
|
readonly gatewayServiceUrl: string;
|
|
184
|
+
/** The MCP-Auth-Proxy service's `run.app` URL (public on gcp-firebase). */
|
|
185
|
+
readonly mcpAuthProxyServiceUrl: string;
|
|
169
186
|
}
|
|
170
187
|
|
|
171
188
|
/**
|
|
172
189
|
* The origins the probe calls. On gcp the admin's IAP lives on the load
|
|
173
|
-
* balancer (`admin.<domain>`) and the gateway's ingress is
|
|
174
|
-
* (`<domain>/api/*`); on
|
|
175
|
-
*
|
|
190
|
+
* balancer (`admin.<domain>`), and the gateway's and the proxy's ingress is
|
|
191
|
+
* load-balancer-only (`<domain>/api/*`, `<domain>/api/mcp/*`); on
|
|
192
|
+
* gcp-firebase every `run.app` URL is reachable — the admin's through Cloud
|
|
193
|
+
* Run's own IAP, the others publicly.
|
|
176
194
|
*/
|
|
177
|
-
export function healthOrigins(target: HealthTarget): {
|
|
195
|
+
export function healthOrigins(target: HealthTarget): {
|
|
196
|
+
admin: string;
|
|
197
|
+
gateway: string;
|
|
198
|
+
mcpAuthProxy: string;
|
|
199
|
+
} {
|
|
178
200
|
return target.provider === "gcp"
|
|
179
|
-
? {
|
|
201
|
+
? {
|
|
202
|
+
admin: `https://admin.${target.domain}`,
|
|
203
|
+
gateway: `https://${target.domain}`,
|
|
204
|
+
mcpAuthProxy: `https://${target.domain}`,
|
|
205
|
+
}
|
|
180
206
|
: {
|
|
181
207
|
admin: new URL(target.adminServiceUrl).origin,
|
|
182
208
|
gateway: new URL(target.gatewayServiceUrl).origin,
|
|
209
|
+
mcpAuthProxy: new URL(target.mcpAuthProxyServiceUrl).origin,
|
|
183
210
|
};
|
|
184
211
|
}
|
|
185
212
|
|
|
@@ -437,7 +464,12 @@ export function createIapHealthProbe(target: HealthTarget, deps: HealthProbeDeps
|
|
|
437
464
|
if (!a.ok) return a;
|
|
438
465
|
const gateway = await get(`${origins.gateway}/api/_caelo/health?deep=1`, {});
|
|
439
466
|
if (!gateway.reached) return { ok: false, error: `gateway unreachable: ${gateway.error}` };
|
|
440
|
-
const g =
|
|
441
|
-
|
|
467
|
+
const g = evaluatePublicServiceDeepHealth("gateway", gateway.status, gateway.text);
|
|
468
|
+
if (!g.ok) return g;
|
|
469
|
+
// #1328 — the proxy holds no database login: healthy once it answers.
|
|
470
|
+
const proxy = await get(`${origins.mcpAuthProxy}${MCP_AUTH_PROXY_HEALTH_PATH}`, {});
|
|
471
|
+
if (!proxy.reached) return { ok: false, error: `MCP-Auth-Proxy unreachable: ${proxy.error}` };
|
|
472
|
+
const p = evaluatePublicServiceDeepHealth("MCP-Auth-Proxy", proxy.status, proxy.text);
|
|
473
|
+
return p.ok ? { ok: true, detail: `${a.detail}; ${g.detail}; ${p.detail}` } : p;
|
|
442
474
|
}
|
|
443
475
|
}
|
|
@@ -184,6 +184,40 @@ describe("platformDigestsOf", () => {
|
|
|
184
184
|
expect(calls).toEqual([]);
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
+
it("#1328: carries the MCP-Auth-Proxy's signed form, and skips a proxy digest install.json lacks", async () => {
|
|
188
|
+
const calls: string[] = [];
|
|
189
|
+
const forms = createReleaseImageForms(fakeRegistryFetch(calls));
|
|
190
|
+
const m = {
|
|
191
|
+
version: "0.12.0",
|
|
192
|
+
digests: { admin: D("a"), gateway: D("b"), ops: D("c"), "mcp-auth-proxy": D("d") },
|
|
193
|
+
platformDigests: { admin: D("1"), gateway: D("2"), ops: D("3"), "mcp-auth-proxy": D("4") },
|
|
194
|
+
};
|
|
195
|
+
expect(
|
|
196
|
+
await platformDigestsOf(
|
|
197
|
+
{ admin: D("a"), gateway: D("b"), "mcp-auth-proxy": D("d") },
|
|
198
|
+
forms,
|
|
199
|
+
m,
|
|
200
|
+
),
|
|
201
|
+
).toEqual({
|
|
202
|
+
ok: true,
|
|
203
|
+
platformDigests: { admin: D("1"), gateway: D("2"), "mcp-auth-proxy": D("4") },
|
|
204
|
+
});
|
|
205
|
+
// Recorded before the proxy existed: no proxy form, and nothing looked up for it.
|
|
206
|
+
expect(await platformDigestsOf({ admin: D("a"), gateway: D("b") }, forms, m)).toEqual({
|
|
207
|
+
ok: true,
|
|
208
|
+
platformDigests: { admin: D("1"), gateway: D("2") },
|
|
209
|
+
});
|
|
210
|
+
expect(calls).toEqual([]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("#1328: a release from before the proxy ships no proxy image form", async () => {
|
|
214
|
+
const forms = createReleaseImageForms(fakeRegistryFetch([]));
|
|
215
|
+
const m = { version: "0.11.0", digests: { admin: D("a"), gateway: D("b"), ops: D("c") } };
|
|
216
|
+
const r = await forms.of(m, "mcp-auth-proxy");
|
|
217
|
+
expect(r).toMatchObject({ ok: false, transient: false });
|
|
218
|
+
expect(r.ok ? "" : r.message).toContain("ships no mcp-auth-proxy image");
|
|
219
|
+
});
|
|
220
|
+
|
|
187
221
|
it("says why when an index cannot be read", async () => {
|
|
188
222
|
const forms = createReleaseImageForms(fakeRegistryFetch([], { status: 503 }));
|
|
189
223
|
const r = await platformDigestsOf({ admin: ADMIN_INDEX, gateway: GATEWAY_INDEX }, forms);
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
import { z } from "zod";
|
|
31
31
|
import type { ImageDigests } from "../install-state.js";
|
|
32
32
|
import type { ReleaseManifest } from "../release-manifest.js";
|
|
33
|
-
import type
|
|
33
|
+
import { ROLL_ORDER, type RollSlug } from "./app-roll.js";
|
|
34
34
|
import { fetchRegistryManifest, IMAGE_DIGEST, RELEASE_REGISTRY } from "./release-images.js";
|
|
35
35
|
|
|
36
36
|
/** The digests one release image is known by. */
|
|
@@ -178,12 +178,20 @@ export function createReleaseImageForms(fetchImpl: typeof fetch = fetch): Releas
|
|
|
178
178
|
return {
|
|
179
179
|
ofIndex,
|
|
180
180
|
of: async (m, slug) => {
|
|
181
|
+
const index = m.digests[slug];
|
|
182
|
+
if (!index) {
|
|
183
|
+
// #1328 — a release from before the MCP-Auth-Proxy signs no image of it.
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
transient: false,
|
|
187
|
+
message: `release ${m.version} ships no ${slug} image`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
181
190
|
const signed = m.platformDigests?.[slug];
|
|
182
191
|
if (signed) {
|
|
183
|
-
const index = m.digests[slug];
|
|
184
192
|
return { ok: true, digests: [index, ...(signed === index ? [] : [signed])], amd64: signed };
|
|
185
193
|
}
|
|
186
|
-
return ofIndex(slug,
|
|
194
|
+
return ofIndex(slug, index);
|
|
187
195
|
},
|
|
188
196
|
};
|
|
189
197
|
}
|
|
@@ -205,22 +213,29 @@ export async function platformDigestsOf(
|
|
|
205
213
|
| { readonly ok: false; readonly message: string }
|
|
206
214
|
> {
|
|
207
215
|
const out: Partial<Record<RollSlug, string>> = {};
|
|
208
|
-
for (const slug of
|
|
216
|
+
for (const slug of ROLL_ORDER) {
|
|
217
|
+
const index = digests[slug];
|
|
218
|
+
// #1328 — digests recorded before the MCP-Auth-Proxy existed carry none.
|
|
219
|
+
if (!index) continue;
|
|
209
220
|
const found =
|
|
210
|
-
manifest && manifest.digests[slug] ===
|
|
221
|
+
manifest && manifest.digests[slug] === index
|
|
211
222
|
? await forms.of(manifest, slug)
|
|
212
|
-
: await forms.ofIndex(slug,
|
|
223
|
+
: await forms.ofIndex(slug, index);
|
|
213
224
|
if (!found.ok) return { ok: false, message: found.message };
|
|
214
225
|
if (!found.amd64) {
|
|
215
226
|
return {
|
|
216
227
|
ok: false,
|
|
217
|
-
message: `the ${slug} image index ${
|
|
228
|
+
message: `the ${slug} image index ${index} lists no single linux/amd64 manifest`,
|
|
218
229
|
};
|
|
219
230
|
}
|
|
220
231
|
out[slug] = found.amd64;
|
|
221
232
|
}
|
|
222
233
|
const { admin, gateway } = out;
|
|
234
|
+
const proxy = out["mcp-auth-proxy"];
|
|
223
235
|
return admin && gateway
|
|
224
|
-
? {
|
|
236
|
+
? {
|
|
237
|
+
ok: true,
|
|
238
|
+
platformDigests: { admin, gateway, ...(proxy ? { "mcp-auth-proxy": proxy } : {}) },
|
|
239
|
+
}
|
|
225
240
|
: { ok: false, message: "the platform digests could not be read" };
|
|
226
241
|
}
|
|
@@ -70,7 +70,11 @@ describe("resolveRegistryDigest", () => {
|
|
|
70
70
|
const missing = fakeFetch(() => new Response(null, { status: 404, statusText: "Not Found" }));
|
|
71
71
|
expect(
|
|
72
72
|
await resolveRegistryDigest("ghcr.io", "caelo-cms/admin", "9.9.9", missing.impl),
|
|
73
|
-
).toEqual({ ok: false, reason: "HTTP 404 Not Found" });
|
|
73
|
+
).toEqual({ ok: false, reason: "HTTP 404 Not Found", notFound: true });
|
|
74
|
+
// Only a 404 is the verdict "no such tag" (#1328); an outage is not.
|
|
75
|
+
const down = fakeFetch(() => new Response(null, { status: 503 }));
|
|
76
|
+
const outage = await resolveRegistryDigest("ghcr.io", "caelo-cms/admin", "9.9.9", down.impl);
|
|
77
|
+
expect(outage.ok === false && "notFound" in outage).toBe(false);
|
|
74
78
|
const bad = fakeFetch(
|
|
75
79
|
() => new Response(null, { status: 200, headers: { "docker-content-digest": "md5:x" } }),
|
|
76
80
|
);
|