@caelo-cms/provisioning 0.10.46 → 0.10.48
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/adc-impersonation.d.ts.map +1 -1
- package/dist/adc-impersonation.js +23 -1
- package/dist/adc-impersonation.js.map +1 -1
- package/dist/agent/apply.d.ts +45 -0
- package/dist/agent/apply.d.ts.map +1 -0
- package/dist/agent/apply.js +83 -0
- package/dist/agent/apply.js.map +1 -0
- package/dist/agent/cli-provenance.d.ts +42 -0
- package/dist/agent/cli-provenance.d.ts.map +1 -0
- package/dist/agent/cli-provenance.js +138 -0
- package/dist/agent/cli-provenance.js.map +1 -0
- package/dist/agent/command.d.ts +79 -0
- package/dist/agent/command.d.ts.map +1 -0
- package/dist/agent/command.js +454 -0
- package/dist/agent/command.js.map +1 -0
- package/dist/agent/config.d.ts +30 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +60 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/lock.d.ts +13 -0
- package/dist/agent/lock.d.ts.map +1 -0
- package/dist/agent/lock.js +100 -0
- package/dist/agent/lock.js.map +1 -0
- package/dist/agent/queue.d.ts +35 -0
- package/dist/agent/queue.d.ts.map +1 -0
- package/dist/agent/queue.js +50 -0
- package/dist/agent/queue.js.map +1 -0
- package/dist/agent/run.d.ts +97 -0
- package/dist/agent/run.d.ts.map +1 -0
- package/dist/agent/run.js +198 -0
- package/dist/agent/run.js.map +1 -0
- package/dist/agent/systemd.d.ts +55 -0
- package/dist/agent/systemd.d.ts.map +1 -0
- package/dist/agent/systemd.js +171 -0
- package/dist/agent/systemd.js.map +1 -0
- package/dist/agent/units.d.ts +44 -0
- package/dist/agent/units.d.ts.map +1 -0
- package/dist/agent/units.js +127 -0
- package/dist/agent/units.js.map +1 -0
- package/dist/agent-db.js +18362 -0
- package/dist/cli.js +122 -16
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-iam-user.d.ts +39 -0
- package/dist/cloudsql-iam-user.d.ts.map +1 -0
- package/dist/cloudsql-iam-user.js +119 -0
- package/dist/cloudsql-iam-user.js.map +1 -0
- package/dist/compose.d.ts +16 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +26 -1
- package/dist/compose.js.map +1 -1
- package/dist/dns/index.d.ts +2 -0
- package/dist/dns/index.d.ts.map +1 -1
- package/dist/dns/index.js +3 -2
- package/dist/dns/index.js.map +1 -1
- package/dist/dns/manual.d.ts +5 -0
- package/dist/dns/manual.d.ts.map +1 -1
- package/dist/dns/manual.js +28 -12
- package/dist/dns/manual.js.map +1 -1
- package/dist/gcloud.d.ts +2 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +1 -1
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +15 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +23 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.js +7 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job-runner.d.ts +13 -0
- package/dist/install-job-runner.d.ts.map +1 -0
- package/dist/install-job-runner.js +155 -0
- package/dist/install-job-runner.js.map +1 -0
- package/dist/install-job.d.ts +210 -0
- package/dist/install-job.d.ts.map +1 -0
- package/dist/install-job.js +449 -0
- package/dist/install-job.js.map +1 -0
- package/dist/install-state.d.ts +19 -1
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +31 -3
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +24 -5
- package/dist/lifecycle.js.map +1 -1
- package/dist/migration-runner.d.ts +5 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +1 -1
- package/dist/migration-runner.js.map +1 -1
- package/dist/operator-access.d.ts +14 -8
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +16 -102
- package/dist/operator-access.js.map +1 -1
- package/dist/run-mode.d.ts +27 -0
- package/dist/run-mode.d.ts.map +1 -0
- package/dist/run-mode.js +22 -0
- package/dist/run-mode.js.map +1 -0
- package/dist/runtime-secrets.d.ts +19 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +44 -0
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +22 -1
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +81 -12
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +17 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +16 -1
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +6 -0
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +10 -1
- package/dist/stack-contract.js.map +1 -1
- package/dist/state-attach.d.ts +23 -0
- package/dist/state-attach.d.ts.map +1 -0
- package/dist/state-attach.js +112 -0
- package/dist/state-attach.js.map +1 -0
- package/dist/state-backend.d.ts +11 -0
- package/dist/state-backend.d.ts.map +1 -1
- package/dist/state-backend.js +20 -0
- package/dist/state-backend.js.map +1 -1
- package/dist/state-command.d.ts +20 -0
- package/dist/state-command.d.ts.map +1 -0
- package/dist/state-command.js +237 -0
- package/dist/state-command.js.map +1 -0
- package/dist/state-migrate.d.ts +121 -0
- package/dist/state-migrate.d.ts.map +1 -0
- package/dist/state-migrate.js +290 -0
- package/dist/state-migrate.js.map +1 -0
- package/dist/state-pulumi.d.ts +17 -0
- package/dist/state-pulumi.d.ts.map +1 -0
- package/dist/state-pulumi.js +218 -0
- package/dist/state-pulumi.js.map +1 -0
- package/dist/state-store.d.ts +27 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +65 -0
- package/dist/state-store.js.map +1 -1
- package/dist/upgrade/converge.d.ts +6 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +59 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +6 -0
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +25 -0
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +31 -0
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +5 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +28 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +82 -0
- package/dist/upgrader-assess.d.ts.map +1 -0
- package/dist/upgrader-assess.js +116 -0
- package/dist/upgrader-assess.js.map +1 -0
- package/dist/upgrader-gcp.d.ts +17 -0
- package/dist/upgrader-gcp.d.ts.map +1 -0
- package/dist/upgrader-gcp.js +87 -0
- package/dist/upgrader-gcp.js.map +1 -0
- package/dist/upgrader-job.d.ts +131 -0
- package/dist/upgrader-job.d.ts.map +1 -0
- package/dist/upgrader-job.js +259 -0
- package/dist/upgrader-job.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +35 -0
- package/dist/upgrader-reporter.d.ts.map +1 -0
- package/dist/upgrader-reporter.js +79 -0
- package/dist/upgrader-reporter.js.map +1 -0
- package/dist/upgrader-setup.d.ts +59 -0
- package/dist/upgrader-setup.d.ts.map +1 -0
- package/dist/upgrader-setup.js +308 -0
- package/dist/upgrader-setup.js.map +1 -0
- package/dist/wizard.d.ts +6 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +32 -4
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-firebase.d.ts +3 -0
- package/dist/wizards/gcp-firebase.d.ts.map +1 -1
- package/dist/wizards/gcp-firebase.js +1 -0
- package/dist/wizards/gcp-firebase.js.map +1 -1
- package/dist/wizards/gcp-install-job.d.ts +74 -0
- package/dist/wizards/gcp-install-job.d.ts.map +1 -0
- package/dist/wizards/gcp-install-job.js +398 -0
- package/dist/wizards/gcp-install-job.js.map +1 -0
- package/dist/wizards/gcp-prompts.d.ts +3 -0
- package/dist/wizards/gcp-prompts.d.ts.map +1 -0
- package/dist/wizards/gcp-prompts.js +25 -0
- package/dist/wizards/gcp-prompts.js.map +1 -0
- package/dist/wizards/gcp-pulumi.d.ts +9 -2
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +8 -6
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts +40 -0
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +291 -83
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +6 -4
- package/src/adapter.ts +97 -0
- package/src/adc-impersonation.ts +176 -0
- package/src/admin-domain.test.ts +254 -0
- package/src/admin-domain.ts +245 -0
- package/src/admin-memory.test.ts +151 -0
- package/src/agent/apply.test.ts +185 -0
- package/src/agent/apply.ts +124 -0
- package/src/agent/cli-provenance.test.ts +159 -0
- package/src/agent/cli-provenance.ts +191 -0
- package/src/agent/command.test.ts +77 -0
- package/src/agent/command.ts +564 -0
- package/src/agent/config.ts +86 -0
- package/src/agent/lock-config.test.ts +97 -0
- package/src/agent/lock.ts +110 -0
- package/src/agent/queue.test.ts +10 -0
- package/src/agent/queue.ts +91 -0
- package/src/agent/run.test.ts +382 -0
- package/src/agent/run.ts +295 -0
- package/src/agent/systemd.ts +209 -0
- package/src/agent/units.test.ts +223 -0
- package/src/agent/units.ts +163 -0
- package/src/azure-front-door.test.ts +119 -0
- package/src/azure-front-door.ts +56 -0
- package/src/bootstrap-token.ts +20 -0
- package/src/caddy.ts +112 -0
- package/src/cdn-copy.ts +91 -0
- package/src/cert-wait.test.ts +194 -0
- package/src/cert-wait.ts +163 -0
- package/src/checkpoint-secrets.test.ts +380 -0
- package/src/checkpoint-secrets.ts +283 -0
- package/src/cli-routing.test.ts +45 -0
- package/src/cli-routing.ts +43 -0
- package/src/cli.ts +1491 -0
- package/src/cloudsql-iam-user.ts +147 -0
- package/src/compose.ts +334 -0
- package/src/default-firewall.test.ts +411 -0
- package/src/default-firewall.ts +342 -0
- package/src/deployed-release.test.ts +231 -0
- package/src/deployed-release.ts +220 -0
- package/src/dns/cloudflare.ts +182 -0
- package/src/dns/groups.test.ts +76 -0
- package/src/dns/groups.ts +86 -0
- package/src/dns/index.ts +56 -0
- package/src/dns/manual.ts +130 -0
- package/src/dns/types.ts +27 -0
- package/src/gateway-credentials.test.ts +180 -0
- package/src/gateway-credentials.ts +144 -0
- package/src/gcloud-components.test.ts +76 -0
- package/src/gcloud-components.ts +85 -0
- package/src/gcloud-retry.ts +41 -0
- package/src/gcloud-runner.test.ts +61 -0
- package/src/gcloud.test.ts +99 -0
- package/src/gcloud.ts +322 -0
- package/src/gcp-names.ts +135 -0
- package/src/index.test.ts +332 -0
- package/src/index.ts +114 -0
- package/src/infra-contract.ts +32 -0
- package/src/install-job-runner.ts +166 -0
- package/src/install-job.test.ts +566 -0
- package/src/install-job.ts +542 -0
- package/src/install-state.test.ts +239 -0
- package/src/install-state.ts +583 -0
- package/src/internal-token.ts +35 -0
- package/src/lifecycle.ts +900 -0
- package/src/mcp-iap-stacks.test.ts +58 -0
- package/src/mcp-iap.test.ts +149 -0
- package/src/mcp-iap.ts +131 -0
- package/src/media-volume.test.ts +296 -0
- package/src/migration-runner.ts +365 -0
- package/src/operator-access-stacks.test.ts +55 -0
- package/src/operator-access.test.ts +332 -0
- package/src/operator-access.ts +554 -0
- package/src/ops-backend.test.ts +320 -0
- package/src/ops-backend.ts +414 -0
- package/src/provisioner-identity.test.ts +482 -0
- package/src/provisioner-identity.ts +407 -0
- package/src/redirects-emit.ts +175 -0
- package/src/region-data.ts +428 -0
- package/src/regions.test.ts +277 -0
- package/src/regions.ts +370 -0
- package/src/release-contract.ts +57 -0
- package/src/release-manifest.test.ts +293 -0
- package/src/release-manifest.ts +430 -0
- package/src/run-mode.ts +40 -0
- package/src/runtime-secrets.test.ts +627 -0
- package/src/runtime-secrets.ts +754 -0
- package/src/self-command.test.ts +14 -0
- package/src/self-command.ts +21 -0
- package/src/self-hosted-compose.test.ts +177 -0
- package/src/self-hosted-release.ts +240 -0
- package/src/self-hosted-roles.integration.test.ts +161 -0
- package/src/self-hosted-roles.test.ts +124 -0
- package/src/self-hosted-roles.ts +203 -0
- package/src/self-hosted-upgrade.test.ts +394 -0
- package/src/self-hosted-upgrade.ts +548 -0
- package/src/stack-contract-parity.test.ts +199 -0
- package/src/stack-contract.test.ts +391 -0
- package/src/stack-contract.ts +888 -0
- package/src/stack-converge-secrets.test.ts +421 -0
- package/src/stack-converge.test.ts +833 -0
- package/src/stack-converge.ts +909 -0
- package/src/staging-admin.test.ts +246 -0
- package/src/staging-admin.ts +324 -0
- package/src/state-attach.test.ts +211 -0
- package/src/state-attach.ts +132 -0
- package/src/state-backend.test.ts +145 -0
- package/src/state-backend.ts +262 -0
- package/src/state-command.ts +293 -0
- package/src/state-migrate.test.ts +479 -0
- package/src/state-migrate.ts +434 -0
- package/src/state-pulumi.ts +289 -0
- package/src/state-store.test.ts +212 -0
- package/src/state-store.ts +243 -0
- package/src/upgrade/app-roll.ts +486 -0
- package/src/upgrade/backup.ts +109 -0
- package/src/upgrade/converge.ts +437 -0
- package/src/upgrade/gcp-lookup.ts +167 -0
- package/src/upgrade/health.test.ts +279 -0
- package/src/upgrade/health.ts +305 -0
- package/src/upgrade/release-images.test.ts +119 -0
- package/src/upgrade/release-images.ts +391 -0
- package/src/upgrade/reporter.ts +46 -0
- package/src/upgrade/upgrade.test.ts +626 -0
- package/src/upgrade/upgrade.ts +249 -0
- package/src/upgrader-assess.test.ts +265 -0
- package/src/upgrader-assess.ts +233 -0
- package/src/upgrader-gcp.ts +123 -0
- package/src/upgrader-job.test.ts +408 -0
- package/src/upgrader-job.ts +499 -0
- package/src/upgrader-reporter.test.ts +46 -0
- package/src/upgrader-reporter.ts +105 -0
- package/src/upgrader-setup.test.ts +169 -0
- package/src/upgrader-setup.ts +355 -0
- package/src/wizard.ts +425 -0
- package/src/wizards/gcp-cost.ts +188 -0
- package/src/wizards/gcp-firebase.ts +72 -0
- package/src/wizards/gcp-install-job.ts +506 -0
- package/src/wizards/gcp-prompts.ts +26 -0
- package/src/wizards/gcp-pulumi.test.ts +246 -0
- package/src/wizards/gcp-pulumi.ts +516 -0
- package/src/wizards/gcp.ts +1935 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import type { GcloudResult } from "../gcloud.js";
|
|
5
|
+
import type { HttpFetch } from "../runtime-secrets.js";
|
|
6
|
+
import {
|
|
7
|
+
createIapHealthProbe,
|
|
8
|
+
evaluateAdminDeepHealth,
|
|
9
|
+
evaluateGatewayDeepHealth,
|
|
10
|
+
type HealthTarget,
|
|
11
|
+
healthOrigins,
|
|
12
|
+
} from "./health.js";
|
|
13
|
+
|
|
14
|
+
const migrations = (adminPending = 0, publicPending = 0) => ({
|
|
15
|
+
admin: { head: "0262_tool_approval_branch.sql", pending: adminPending },
|
|
16
|
+
public: { head: "0040_x.sql", pending: publicPending },
|
|
17
|
+
});
|
|
18
|
+
const adminBody = (over: Record<string, unknown> = {}) =>
|
|
19
|
+
JSON.stringify({ ok: true, version: "0.11.0", migrations: migrations(), ...over });
|
|
20
|
+
|
|
21
|
+
describe("evaluateAdminDeepHealth", () => {
|
|
22
|
+
it("accepts a healthy admin on the targeted version with every migration applied", () => {
|
|
23
|
+
const r = evaluateAdminDeepHealth(200, adminBody(), "0.11.0");
|
|
24
|
+
expect(r).toEqual({
|
|
25
|
+
ok: true,
|
|
26
|
+
version: "0.11.0",
|
|
27
|
+
detail: "admin 0.11.0, migrations at 0262_tool_approval_branch.sql / 0040_x.sql",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("names the database that misses migrations", () => {
|
|
32
|
+
const r = evaluateAdminDeepHealth(
|
|
33
|
+
503,
|
|
34
|
+
adminBody({ ok: false, migrations: migrations(0, 2) }),
|
|
35
|
+
null,
|
|
36
|
+
);
|
|
37
|
+
expect(r).toEqual({
|
|
38
|
+
ok: false,
|
|
39
|
+
error:
|
|
40
|
+
"cms_public is missing 2 migration(s) the new admin image ships (applied head: 0040_x.sql)",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("refuses a different version than the one rolled", () => {
|
|
45
|
+
const r = evaluateAdminDeepHealth(200, adminBody({ version: "0.10.43" }), "0.11.0");
|
|
46
|
+
expect(r.ok).toBe(false);
|
|
47
|
+
if (!r.ok) expect(r.error).toBe("admin runs 0.10.43, the roll targeted 0.11.0");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("refuses an image without the deep check and points at --skip-health-check", () => {
|
|
51
|
+
const r = evaluateAdminDeepHealth(200, JSON.stringify({ ok: true, version: "0.11.0" }), null);
|
|
52
|
+
expect(r.ok).toBe(false);
|
|
53
|
+
if (!r.ok) expect(r.error).toContain("--skip-health-check");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("reports an unreachable database", () => {
|
|
57
|
+
const r = evaluateAdminDeepHealth(
|
|
58
|
+
503,
|
|
59
|
+
JSON.stringify({
|
|
60
|
+
ok: false,
|
|
61
|
+
version: "0.11.0",
|
|
62
|
+
error: { kind: "DbUnreachable", message: "m" },
|
|
63
|
+
}),
|
|
64
|
+
null,
|
|
65
|
+
);
|
|
66
|
+
expect(r).toEqual({ ok: false, error: "admin reports unhealthy (HTTP 503, DbUnreachable: m)" });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("treats an HTML page (IAP login) as a failure", () => {
|
|
70
|
+
expect(evaluateAdminDeepHealth(200, "<html>Sign in</html>", null).ok).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("evaluateGatewayDeepHealth", () => {
|
|
75
|
+
it("accepts a healthy gateway on the admin's version", () => {
|
|
76
|
+
expect(
|
|
77
|
+
evaluateGatewayDeepHealth(200, JSON.stringify({ ok: true, version: "0.11.0" }), "0.11.0"),
|
|
78
|
+
).toEqual({ ok: true, detail: "gateway 0.11.0" });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("refuses a gateway on another version than the admin", () => {
|
|
82
|
+
const r = evaluateGatewayDeepHealth(
|
|
83
|
+
200,
|
|
84
|
+
JSON.stringify({ ok: true, version: "0.10.0" }),
|
|
85
|
+
"0.11.0",
|
|
86
|
+
);
|
|
87
|
+
expect(r.ok).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("refuses a gateway that can't reach its database", () => {
|
|
91
|
+
const r = evaluateGatewayDeepHealth(
|
|
92
|
+
503,
|
|
93
|
+
JSON.stringify({ ok: false, version: "0.11.0", error: { kind: "DbUnreachable" } }),
|
|
94
|
+
"0.11.0",
|
|
95
|
+
);
|
|
96
|
+
expect(r).toEqual({ ok: false, error: "gateway reports unhealthy (HTTP 503, DbUnreachable)" });
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const FIREBASE: HealthTarget = {
|
|
101
|
+
provider: "gcp-firebase",
|
|
102
|
+
projectId: "acme",
|
|
103
|
+
domain: "acme.com",
|
|
104
|
+
adminServiceUrl: "https://caelo-production-admin-aaa-xyz.a.run.app",
|
|
105
|
+
gatewayServiceUrl: "https://caelo-production-gateway-bbb-xyz.a.run.app",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
describe("healthOrigins", () => {
|
|
109
|
+
it("uses the load balancer hosts on gcp", () => {
|
|
110
|
+
expect(healthOrigins({ ...FIREBASE, provider: "gcp" })).toEqual({
|
|
111
|
+
admin: "https://admin.acme.com",
|
|
112
|
+
gateway: "https://acme.com",
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("uses the run.app URLs on gcp-firebase", () => {
|
|
117
|
+
expect(healthOrigins(FIREBASE)).toEqual({
|
|
118
|
+
admin: "https://caelo-production-admin-aaa-xyz.a.run.app",
|
|
119
|
+
gateway: "https://caelo-production-gateway-bbb-xyz.a.run.app",
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("createIapHealthProbe", () => {
|
|
125
|
+
const gcloudOk = async (): Promise<GcloudResult> => ({
|
|
126
|
+
ok: true,
|
|
127
|
+
stdout: "ya29.token\n",
|
|
128
|
+
stderr: "",
|
|
129
|
+
exitCode: 0,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/** A fake network: signJwt + the two health endpoints. */
|
|
133
|
+
function network(answers: { admin: () => [number, string]; gateway: () => [number, string] }) {
|
|
134
|
+
const requests: { url: string; headers: Record<string, string>; body?: string }[] = [];
|
|
135
|
+
const http: HttpFetch = async (url, init) => {
|
|
136
|
+
requests.push({ url, headers: init.headers, ...(init.body ? { body: init.body } : {}) });
|
|
137
|
+
const [status, text] = url.includes(":signJwt")
|
|
138
|
+
? [200, JSON.stringify({ signedJwt: "signed.jwt" })]
|
|
139
|
+
: url.includes("admin")
|
|
140
|
+
? answers.admin()
|
|
141
|
+
: answers.gateway();
|
|
142
|
+
return { ok: status < 400, status, text: async () => text };
|
|
143
|
+
};
|
|
144
|
+
return { http, requests };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
it("signs an IAP credential for the admin origin and sends it to the admin only", async () => {
|
|
148
|
+
const net = network({
|
|
149
|
+
admin: () => [200, adminBody()],
|
|
150
|
+
gateway: () => [200, JSON.stringify({ ok: true, version: "0.11.0" })],
|
|
151
|
+
});
|
|
152
|
+
const probe = createIapHealthProbe(FIREBASE, {
|
|
153
|
+
run: gcloudOk,
|
|
154
|
+
http: net.http,
|
|
155
|
+
sleep: async () => {},
|
|
156
|
+
now: () => 1000,
|
|
157
|
+
});
|
|
158
|
+
expect(await probe.deep({ version: "0.11.0" })).toEqual({
|
|
159
|
+
ok: true,
|
|
160
|
+
detail:
|
|
161
|
+
"admin 0.11.0, migrations at 0262_tool_approval_branch.sql / 0040_x.sql; gateway 0.11.0",
|
|
162
|
+
});
|
|
163
|
+
const sign = net.requests[0];
|
|
164
|
+
expect(sign?.url).toBe(
|
|
165
|
+
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/caelo-mcp%40acme.iam.gserviceaccount.com:signJwt",
|
|
166
|
+
);
|
|
167
|
+
const claims = JSON.parse((JSON.parse(sign?.body ?? "{}") as { payload: string }).payload);
|
|
168
|
+
expect(claims).toEqual({
|
|
169
|
+
iss: "caelo-mcp@acme.iam.gserviceaccount.com",
|
|
170
|
+
sub: "caelo-mcp@acme.iam.gserviceaccount.com",
|
|
171
|
+
aud: "https://caelo-production-admin-aaa-xyz.a.run.app/*",
|
|
172
|
+
iat: 1000,
|
|
173
|
+
exp: 1600,
|
|
174
|
+
});
|
|
175
|
+
const admin = net.requests.find(
|
|
176
|
+
(r) => r.url.endsWith("/_caelo/health?deep=1") && r.url.includes("admin"),
|
|
177
|
+
);
|
|
178
|
+
expect(admin?.headers.Authorization).toBe("Bearer signed.jwt");
|
|
179
|
+
const gateway = net.requests.find((r) => r.url.endsWith("/api/_caelo/health?deep=1"));
|
|
180
|
+
expect(gateway?.url).toBe(
|
|
181
|
+
"https://caelo-production-gateway-bbb-xyz.a.run.app/api/_caelo/health?deep=1",
|
|
182
|
+
);
|
|
183
|
+
expect(gateway?.headers.Authorization).toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("retries while the new revisions warm up, then gives the last error", async () => {
|
|
187
|
+
let calls = 0;
|
|
188
|
+
const net = network({
|
|
189
|
+
admin: () => {
|
|
190
|
+
calls++;
|
|
191
|
+
return calls < 3
|
|
192
|
+
? [503, adminBody({ ok: false, migrations: migrations(1) })]
|
|
193
|
+
: [200, adminBody()];
|
|
194
|
+
},
|
|
195
|
+
gateway: () => [200, JSON.stringify({ ok: true, version: "0.11.0" })],
|
|
196
|
+
});
|
|
197
|
+
const probe = createIapHealthProbe(FIREBASE, {
|
|
198
|
+
run: gcloudOk,
|
|
199
|
+
http: net.http,
|
|
200
|
+
sleep: async () => {},
|
|
201
|
+
attempts: 5,
|
|
202
|
+
});
|
|
203
|
+
expect((await probe.deep({ version: null })).ok).toBe(true);
|
|
204
|
+
expect(calls).toBe(3);
|
|
205
|
+
|
|
206
|
+
const stuck = network({
|
|
207
|
+
admin: () => [503, adminBody({ ok: false, migrations: migrations(1) })],
|
|
208
|
+
gateway: () => [200, "{}"],
|
|
209
|
+
});
|
|
210
|
+
const failing = createIapHealthProbe(FIREBASE, {
|
|
211
|
+
run: gcloudOk,
|
|
212
|
+
http: stuck.http,
|
|
213
|
+
sleep: async () => {},
|
|
214
|
+
attempts: 2,
|
|
215
|
+
});
|
|
216
|
+
const r = await failing.deep({ version: null });
|
|
217
|
+
expect(r.ok).toBe(false);
|
|
218
|
+
if (!r.ok) expect(r.error).toContain("cms_admin is missing 1 migration(s)");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("baseline fails when IAP answers instead of the admin", async () => {
|
|
222
|
+
const net = network({
|
|
223
|
+
admin: () => [200, "<html>Sign in with Google</html>"],
|
|
224
|
+
gateway: () => [404, "{}"],
|
|
225
|
+
});
|
|
226
|
+
const probe = createIapHealthProbe(FIREBASE, {
|
|
227
|
+
run: gcloudOk,
|
|
228
|
+
http: net.http,
|
|
229
|
+
sleep: async () => {},
|
|
230
|
+
});
|
|
231
|
+
const r = await probe.baseline();
|
|
232
|
+
expect(r.ok).toBe(false);
|
|
233
|
+
if (!r.ok)
|
|
234
|
+
expect(r.error).toContain("IAP may not admit caelo-mcp@acme.iam.gserviceaccount.com");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("baseline succeeds when the old admin answers its shallow check and the gateway answers at all", async () => {
|
|
238
|
+
const net = network({
|
|
239
|
+
admin: () => [200, JSON.stringify({ ok: true, version: "0.10.43" })],
|
|
240
|
+
gateway: () => [404, '{"ok":false}'],
|
|
241
|
+
});
|
|
242
|
+
const probe = createIapHealthProbe(FIREBASE, {
|
|
243
|
+
run: gcloudOk,
|
|
244
|
+
http: net.http,
|
|
245
|
+
sleep: async () => {},
|
|
246
|
+
});
|
|
247
|
+
expect(await probe.baseline()).toEqual({
|
|
248
|
+
ok: true,
|
|
249
|
+
detail: "admin 0.10.43 and gateway reachable",
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("baseline fails with the cause when the operator can't sign as the MCP account", async () => {
|
|
254
|
+
const http: HttpFetch = async () => ({
|
|
255
|
+
ok: false,
|
|
256
|
+
status: 403,
|
|
257
|
+
text: async () => "Permission iam.serviceAccounts.signJwt denied",
|
|
258
|
+
});
|
|
259
|
+
const probe = createIapHealthProbe(FIREBASE, { run: gcloudOk, http, sleep: async () => {} });
|
|
260
|
+
const r = await probe.baseline();
|
|
261
|
+
expect(r.ok).toBe(false);
|
|
262
|
+
if (!r.ok) expect(r.error).toContain("roles/iam.serviceAccountTokenCreator");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("a transport failure while signing is a failed check, not a throw", async () => {
|
|
266
|
+
const http: HttpFetch = async () => {
|
|
267
|
+
throw new Error("ECONNRESET");
|
|
268
|
+
};
|
|
269
|
+
const probe = createIapHealthProbe(FIREBASE, {
|
|
270
|
+
run: gcloudOk,
|
|
271
|
+
http,
|
|
272
|
+
sleep: async () => {},
|
|
273
|
+
attempts: 2,
|
|
274
|
+
});
|
|
275
|
+
const r = await probe.deep({ version: null });
|
|
276
|
+
expect(r.ok).toBe(false);
|
|
277
|
+
if (!r.ok) expect(r.error).toContain("ECONNRESET");
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deep health check after an app-roll (epic #696).
|
|
5
|
+
*
|
|
6
|
+
* Cloud Run's readiness only proves the new container listens on its port. A
|
|
7
|
+
* revision can do that while it cannot reach the database, runs against a
|
|
8
|
+
* schema its migrations never reached, or is not the release the roll asked
|
|
9
|
+
* for. After both services moved, the app-roll asks them:
|
|
10
|
+
*
|
|
11
|
+
* - admin `GET /_caelo/health?deep=1` → `{ ok, version, migrations }`:
|
|
12
|
+
* both databases reachable as the expected roles, and every
|
|
13
|
+
* migration the image ships recorded as applied.
|
|
14
|
+
* - gateway `GET /api/_caelo/health?deep=1` → `{ ok, version }`: its
|
|
15
|
+
* database logins work. Under `/api/` because that is the only
|
|
16
|
+
* path the gcp load balancer routes to the gateway.
|
|
17
|
+
*
|
|
18
|
+
* Both answer with no secrets and no raw database errors. The admin sits
|
|
19
|
+
* behind IAP, so the probe signs an IAP credential as the install's MCP
|
|
20
|
+
* service account (`caelo-mcp@`, mcp-iap.ts) exactly like the MCP shim does
|
|
21
|
+
* (packages/mcp-server/src/ingress-auth.ts): a JWT with `aud` = the admin
|
|
22
|
+
* origin + `/*`, signed through the IAM Credentials API by whoever runs the
|
|
23
|
+
* upgrade (the converge step lets every IAP-allowlisted operator sign as
|
|
24
|
+
* that account). The gateway is public by design and gets no credential.
|
|
25
|
+
*
|
|
26
|
+
* Before anything changes, {@link HealthProbe.baseline} proves the probe can
|
|
27
|
+
* reach both services at all. Otherwise a DNS, certificate or IAP problem
|
|
28
|
+
* would only show after migrations, as a "failed" health check that rolls a
|
|
29
|
+
* healthy release back.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
|
|
33
|
+
import { mcpIapServiceAccountEmail } from "../gcp-names.js";
|
|
34
|
+
import type { HttpFetch } from "../runtime-secrets.js";
|
|
35
|
+
|
|
36
|
+
export type HealthOutcome =
|
|
37
|
+
| { readonly ok: true; readonly detail: string }
|
|
38
|
+
| { readonly ok: false; readonly error: string };
|
|
39
|
+
|
|
40
|
+
export interface HealthProbe {
|
|
41
|
+
/** Reach both services as they serve now (before any change). */
|
|
42
|
+
baseline(): Promise<HealthOutcome>;
|
|
43
|
+
/**
|
|
44
|
+
* Deep-check both services after the roll, retrying while the new
|
|
45
|
+
* revisions warm up. `version` is the release the roll targets (null when
|
|
46
|
+
* a channel tag like `latest` was rolled: then both must report the same
|
|
47
|
+
* version).
|
|
48
|
+
*/
|
|
49
|
+
deep(expect: { readonly version: string | null }): Promise<HealthOutcome>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface MigrationState {
|
|
53
|
+
readonly head: string | null;
|
|
54
|
+
readonly pending: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What `/_caelo/health?deep=1` answers (apps/admin/src/routes/_caelo/health). */
|
|
58
|
+
interface AdminDeepBody {
|
|
59
|
+
readonly ok?: boolean;
|
|
60
|
+
readonly version?: string;
|
|
61
|
+
readonly migrations?: { readonly admin?: MigrationState; readonly public?: MigrationState };
|
|
62
|
+
readonly error?: { readonly kind?: string; readonly message?: string };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseJson<T>(text: string): T | null {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(text) as T;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Judge the admin's deep health answer.
|
|
75
|
+
*
|
|
76
|
+
* @returns the version the admin runs on success
|
|
77
|
+
*/
|
|
78
|
+
export function evaluateAdminDeepHealth(
|
|
79
|
+
status: number,
|
|
80
|
+
text: string,
|
|
81
|
+
expectedVersion: string | null,
|
|
82
|
+
): { ok: true; version: string; detail: string } | { ok: false; error: string } {
|
|
83
|
+
const body = parseJson<AdminDeepBody>(text);
|
|
84
|
+
if (!body) return { ok: false, error: `admin answered HTTP ${status} without JSON` };
|
|
85
|
+
const m = body.migrations;
|
|
86
|
+
// Pending migrations come with a 503; name them rather than "unhealthy".
|
|
87
|
+
for (const [db, state] of [
|
|
88
|
+
["cms_admin", m?.admin],
|
|
89
|
+
["cms_public", m?.public],
|
|
90
|
+
] as const) {
|
|
91
|
+
if (state && state.pending !== 0) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: `${db} is missing ${state.pending} migration(s) the new admin image ships (applied head: ${state.head ?? "none"})`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (status !== 200 || body.ok !== true) {
|
|
99
|
+
const why = body.error ? `${body.error.kind ?? "?"}: ${body.error.message ?? ""}` : "no reason";
|
|
100
|
+
return { ok: false, error: `admin reports unhealthy (HTTP ${status}, ${why})` };
|
|
101
|
+
}
|
|
102
|
+
if (!body.version) return { ok: false, error: "admin reported no version" };
|
|
103
|
+
if (expectedVersion !== null && body.version !== expectedVersion) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
error: `admin runs ${body.version}, the roll targeted ${expectedVersion}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (!m?.admin || !m.public) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error:
|
|
113
|
+
"admin did not report its migration state (its image predates the deep health check; pass --skip-health-check to roll to such a release)",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
ok: true,
|
|
118
|
+
version: body.version,
|
|
119
|
+
detail: `admin ${body.version}, migrations at ${m.admin.head ?? "?"} / ${m.public.head ?? "?"}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Judge the gateway's deep health answer against the version the admin runs. */
|
|
124
|
+
export function evaluateGatewayDeepHealth(
|
|
125
|
+
status: number,
|
|
126
|
+
text: string,
|
|
127
|
+
expectedVersion: string,
|
|
128
|
+
): HealthOutcome {
|
|
129
|
+
const body = parseJson<{ ok?: boolean; version?: string; error?: { kind?: string } }>(text);
|
|
130
|
+
if (!body) return { ok: false, error: `gateway answered HTTP ${status} without JSON` };
|
|
131
|
+
if (status !== 200 || body.ok !== true) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
error: `gateway reports unhealthy (HTTP ${status}, ${body.error?.kind ?? "no reason"})`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (body.version !== expectedVersion) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
error: `gateway runs ${body.version ?? "an unknown version"}, the admin runs ${expectedVersion}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return { ok: true, detail: `gateway ${body.version}` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Where the probe reaches the install. */
|
|
147
|
+
export interface HealthTarget {
|
|
148
|
+
readonly provider: "gcp" | "gcp-firebase";
|
|
149
|
+
readonly projectId: string;
|
|
150
|
+
readonly domain: string;
|
|
151
|
+
/** The admin service's `run.app` URL (IAP-gated on gcp-firebase). */
|
|
152
|
+
readonly adminServiceUrl: string;
|
|
153
|
+
/** The gateway service's `run.app` URL (public on gcp-firebase). */
|
|
154
|
+
readonly gatewayServiceUrl: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The origins the probe calls. On gcp the admin's IAP lives on the load
|
|
159
|
+
* balancer (`admin.<domain>`) and the gateway's ingress is load-balancer-only
|
|
160
|
+
* (`<domain>/api/*`); on gcp-firebase both `run.app` URLs are reachable — the
|
|
161
|
+
* admin's through Cloud Run's own IAP, the gateway's publicly.
|
|
162
|
+
*/
|
|
163
|
+
export function healthOrigins(target: HealthTarget): { admin: string; gateway: string } {
|
|
164
|
+
return target.provider === "gcp"
|
|
165
|
+
? { admin: `https://admin.${target.domain}`, gateway: `https://${target.domain}` }
|
|
166
|
+
: {
|
|
167
|
+
admin: new URL(target.adminServiceUrl).origin,
|
|
168
|
+
gateway: new URL(target.gatewayServiceUrl).origin,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface HealthProbeDeps {
|
|
173
|
+
readonly run: GcloudRunner;
|
|
174
|
+
readonly http: HttpFetch;
|
|
175
|
+
readonly sleep: Sleep;
|
|
176
|
+
/** Deep-check attempts while the new revisions warm up (default 12). */
|
|
177
|
+
readonly attempts?: number;
|
|
178
|
+
/** Pause between attempts in ms (default 5000). */
|
|
179
|
+
readonly intervalMs?: number;
|
|
180
|
+
/** Seconds since epoch. */
|
|
181
|
+
readonly now?: () => number;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const JWT_LIFETIME_S = 600;
|
|
185
|
+
const IAM_CREDENTIALS = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts";
|
|
186
|
+
|
|
187
|
+
/** The IAP-backed probe of a gcp / gcp-firebase install. */
|
|
188
|
+
export function createIapHealthProbe(target: HealthTarget, deps: HealthProbeDeps): HealthProbe {
|
|
189
|
+
const origins = healthOrigins(target);
|
|
190
|
+
const signer = mcpIapServiceAccountEmail(target.projectId);
|
|
191
|
+
const attempts = deps.attempts ?? 12;
|
|
192
|
+
const intervalMs = deps.intervalMs ?? 5_000;
|
|
193
|
+
const now = deps.now ?? (() => Math.floor(Date.now() / 1000));
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* A JWT IAP accepts for the admin origin, signed as the MCP service
|
|
197
|
+
* account. Never throws: a transport failure is a failed outcome, so the
|
|
198
|
+
* retries and the app-roll's rollback still run.
|
|
199
|
+
*/
|
|
200
|
+
async function iapToken(): Promise<{ ok: true; jwt: string } | { ok: false; error: string }> {
|
|
201
|
+
try {
|
|
202
|
+
return await signIapToken();
|
|
203
|
+
} catch (e) {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
error: `could not sign an IAP credential as ${signer}: ${e instanceof Error ? e.message : String(e)}`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function signIapToken(): Promise<{ ok: true; jwt: string } | { ok: false; error: string }> {
|
|
212
|
+
const token = await deps.run(["auth", "print-access-token"]);
|
|
213
|
+
const bearer = token.ok ? token.stdout.trim() : "";
|
|
214
|
+
if (!bearer) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
error: `gcloud auth print-access-token: ${token.stderr.trim() || "empty"}`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const iat = now();
|
|
221
|
+
const res = await deps.http(`${IAM_CREDENTIALS}/${encodeURIComponent(signer)}:signJwt`, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: { Authorization: `Bearer ${bearer}`, "Content-Type": "application/json" },
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
payload: JSON.stringify({
|
|
226
|
+
iss: signer,
|
|
227
|
+
sub: signer,
|
|
228
|
+
aud: `${origins.admin}/*`,
|
|
229
|
+
iat,
|
|
230
|
+
exp: iat + JWT_LIFETIME_S,
|
|
231
|
+
}),
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
234
|
+
const text = await res.text();
|
|
235
|
+
if (!res.ok) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
error: `could not sign an IAP credential as ${signer} (HTTP ${res.status}). Your account needs roles/iam.serviceAccountTokenCreator on it — upgrade's converge step grants it to every IAP-allowlisted user. ${text.slice(0, 300)}`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const jwt = parseJson<{ signedJwt?: string }>(text)?.signedJwt;
|
|
242
|
+
return jwt ? { ok: true, jwt } : { ok: false, error: "signJwt returned no signedJwt" };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function get(url: string, headers: Record<string, string>) {
|
|
246
|
+
try {
|
|
247
|
+
const res = await deps.http(url, { method: "GET", headers });
|
|
248
|
+
return { reached: true as const, status: res.status, text: await res.text() };
|
|
249
|
+
} catch (e) {
|
|
250
|
+
return { reached: false as const, error: e instanceof Error ? e.message : String(e) };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
async baseline() {
|
|
256
|
+
const token = await iapToken();
|
|
257
|
+
if (!token.ok) return token;
|
|
258
|
+
const auth = { Authorization: `Bearer ${token.jwt}` };
|
|
259
|
+
const admin = await get(`${origins.admin}/_caelo/health`, auth);
|
|
260
|
+
if (!admin.reached) {
|
|
261
|
+
return { ok: false, error: `admin unreachable at ${origins.admin}: ${admin.error}` };
|
|
262
|
+
}
|
|
263
|
+
// The serving (old) admin answers its shallow health JSON. Anything
|
|
264
|
+
// else — IAP's login redirect, a 401/403, an HTML error page — means
|
|
265
|
+
// the probe would not see the new revision either.
|
|
266
|
+
const body = parseJson<{ version?: string }>(admin.text);
|
|
267
|
+
if (!body?.version) {
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
error: `admin at ${origins.admin} did not answer its health check (HTTP ${admin.status}); IAP may not admit ${signer}`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const gateway = await get(`${origins.gateway}/api/_caelo/health`, {});
|
|
274
|
+
if (!gateway.reached) {
|
|
275
|
+
return { ok: false, error: `gateway unreachable at ${origins.gateway}: ${gateway.error}` };
|
|
276
|
+
}
|
|
277
|
+
return { ok: true, detail: `admin ${body.version} and gateway reachable` };
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
async deep(expect) {
|
|
281
|
+
let last: HealthOutcome = { ok: false, error: "no attempt made" };
|
|
282
|
+
for (let i = 0; i < attempts; i++) {
|
|
283
|
+
if (i > 0) await deps.sleep(intervalMs);
|
|
284
|
+
last = await deepOnce(expect.version);
|
|
285
|
+
if (last.ok) return last;
|
|
286
|
+
}
|
|
287
|
+
return last;
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
async function deepOnce(expectedVersion: string | null): Promise<HealthOutcome> {
|
|
292
|
+
const token = await iapToken();
|
|
293
|
+
if (!token.ok) return token;
|
|
294
|
+
const admin = await get(`${origins.admin}/_caelo/health?deep=1`, {
|
|
295
|
+
Authorization: `Bearer ${token.jwt}`,
|
|
296
|
+
});
|
|
297
|
+
if (!admin.reached) return { ok: false, error: `admin unreachable: ${admin.error}` };
|
|
298
|
+
const a = evaluateAdminDeepHealth(admin.status, admin.text, expectedVersion);
|
|
299
|
+
if (!a.ok) return a;
|
|
300
|
+
const gateway = await get(`${origins.gateway}/api/_caelo/health?deep=1`, {});
|
|
301
|
+
if (!gateway.reached) return { ok: false, error: `gateway unreachable: ${gateway.error}` };
|
|
302
|
+
const g = evaluateGatewayDeepHealth(gateway.status, gateway.text, a.version);
|
|
303
|
+
return g.ok ? { ok: true, detail: `${a.detail}; ${g.detail}` } : g;
|
|
304
|
+
}
|
|
305
|
+
}
|