@caelo-cms/provisioning 0.11.9 → 0.11.11
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/adapter.d.ts +6 -0
- package/dist/adapter.d.ts.map +1 -1
- 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 +12 -3
- package/dist/adopt-existing.d.ts.map +1 -1
- package/dist/adopt-existing.js +143 -0
- package/dist/adopt-existing.js.map +1 -1
- package/dist/agent/command.d.ts +23 -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 +69 -10
- 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-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +68 -7
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-tls.d.ts +93 -0
- package/dist/cloudsql-tls.d.ts.map +1 -0
- package/dist/cloudsql-tls.js +295 -0
- package/dist/cloudsql-tls.js.map +1 -0
- package/dist/compose.d.ts +43 -6
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +148 -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-db-trust.d.ts +44 -0
- package/dist/converge-db-trust.d.ts.map +1 -0
- package/dist/converge-db-trust.js +158 -0
- package/dist/converge-db-trust.js.map +1 -0
- package/dist/converge-job-gcp.d.ts +5 -2
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +11 -13
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/converge-job-preflight.d.ts.map +1 -1
- package/dist/converge-job-preflight.js +8 -0
- package/dist/converge-job-preflight.js.map +1 -1
- package/dist/converge-job.d.ts +17 -0
- package/dist/converge-job.d.ts.map +1 -1
- package/dist/converge-job.js +12 -0
- package/dist/converge-job.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 +34 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +45 -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 +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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 +45 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job.d.ts +4 -2
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js +4 -1
- package/dist/install-job.js.map +1 -1
- package/dist/install-state.d.ts +13 -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 +20 -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/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +8 -3
- package/dist/migration-runner.js.map +1 -1
- package/dist/migration-targets.d.ts +11 -0
- package/dist/migration-targets.d.ts.map +1 -0
- package/dist/migration-targets.js +12 -0
- package/dist/migration-targets.js.map +1 -0
- package/dist/operator-access.d.ts +10 -0
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +5 -0
- package/dist/operator-access.js.map +1 -1
- package/dist/pool-budget-stack.d.ts +13 -1
- package/dist/pool-budget-stack.d.ts.map +1 -1
- package/dist/pool-budget-stack.js +11 -1
- package/dist/pool-budget-stack.js.map +1 -1
- package/dist/public-role-isolation.d.ts +7 -3
- package/dist/public-role-isolation.d.ts.map +1 -1
- package/dist/public-role-isolation.js +25 -13
- package/dist/public-role-isolation.js.map +1 -1
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +3 -0
- package/dist/release-contract.js.map +1 -1
- 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 +11 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +40 -25
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/scheduler-setup.d.ts +75 -0
- package/dist/scheduler-setup.d.ts.map +1 -0
- package/dist/scheduler-setup.js +331 -0
- package/dist/scheduler-setup.js.map +1 -0
- 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 +43 -2
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +87 -1
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-scheduler-interval.d.ts +21 -0
- package/dist/self-hosted-scheduler-interval.d.ts.map +1 -0
- package/dist/self-hosted-scheduler-interval.js +68 -0
- package/dist/self-hosted-scheduler-interval.js.map +1 -0
- 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 +19 -7
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +78 -25
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +234 -28
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +397 -39
- 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/staging-database.d.ts +22 -0
- package/dist/staging-database.d.ts.map +1 -0
- package/dist/staging-database.js +117 -0
- package/dist/staging-database.js.map +1 -0
- 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 +5 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +207 -20
- 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 +19 -3
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +37 -2
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +13 -0
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +50 -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/wizard.d.ts +7 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +11 -0
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts +31 -0
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +103 -18
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +129 -27
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +6 -0
- package/src/admin-domain.test.ts +9 -8
- package/src/admin-domain.ts +13 -3
- package/src/admin-memory.test.ts +2 -1
- package/src/adopt-existing.test.ts +85 -0
- package/src/adopt-existing.ts +141 -3
- package/src/agent/command.test.ts +49 -1
- package/src/agent/command.ts +39 -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 +4 -1
- 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-routing.test.ts +1 -1
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +71 -5
- package/src/cloudsql-tls.test.ts +255 -0
- package/src/cloudsql-tls.ts +359 -0
- package/src/compose.ts +193 -24
- package/src/converge-checkpoint.ts +8 -1
- package/src/converge-db-trust.test.ts +237 -0
- package/src/converge-db-trust.ts +212 -0
- package/src/converge-job-gcp.ts +11 -12
- package/src/converge-job-preflight.test.ts +5 -0
- package/src/converge-job-preflight.ts +8 -0
- package/src/converge-job.test.ts +70 -0
- package/src/converge-job.ts +32 -0
- package/src/database-tls-builders.test.ts +56 -0
- package/src/deployed-release.test.ts +107 -40
- package/src/deployed-release.ts +30 -9
- package/src/gateway-env-isolation.test.ts +50 -12
- package/src/gcp-names.ts +56 -0
- package/src/gcp-url-map.test.ts +6 -8
- package/src/gcp-url-map.ts +6 -2
- package/src/index.test.ts +10 -4
- package/src/index.ts +7 -0
- package/src/infra-contract.test.ts +7 -3
- package/src/infra-contract.ts +45 -1
- package/src/install-job.ts +4 -1
- package/src/install-state.ts +33 -4
- package/src/lifecycle.ts +23 -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/migration-runner.ts +14 -9
- package/src/migration-targets.ts +12 -0
- package/src/operator-access-stacks.test.ts +1 -1
- package/src/operator-access.test.ts +20 -4
- package/src/operator-access.ts +14 -0
- package/src/pool-budget-stack.ts +15 -0
- package/src/pool-budget-stacks.test.ts +39 -14
- package/src/public-role-isolation.test.ts +18 -8
- package/src/public-role-isolation.ts +31 -17
- package/src/release-contract.ts +3 -0
- 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 +37 -14
- package/src/runtime-secrets.ts +87 -27
- package/src/scheduler-setup.test.ts +201 -0
- package/src/scheduler-setup.ts +375 -0
- package/src/seal-key-rotation.test.ts +2 -2
- package/src/seal-key-rotation.ts +25 -25
- package/src/self-hosted-backup.test.ts +1 -0
- package/src/self-hosted-compose.test.ts +180 -6
- 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.integration.test.ts +2 -0
- package/src/self-hosted-roles.test.ts +87 -3
- package/src/self-hosted-roles.ts +101 -3
- package/src/self-hosted-scheduler-interval.test.ts +66 -0
- package/src/self-hosted-scheduler-interval.ts +79 -0
- package/src/self-hosted-start.test.ts +35 -2
- package/src/self-hosted-start.ts +20 -2
- package/src/self-hosted-upgrade.test.ts +104 -14
- package/src/self-hosted-upgrade.ts +123 -31
- package/src/stack-contract-parity.test.ts +79 -12
- package/src/stack-contract.test.ts +199 -31
- package/src/stack-contract.ts +472 -45
- package/src/stack-converge-secrets.test.ts +28 -13
- package/src/stack-converge.test.ts +203 -28
- package/src/stack-converge.ts +92 -30
- package/src/staging-database.test.ts +114 -0
- package/src/staging-database.ts +148 -0
- package/src/state-store.test.ts +2 -1
- package/src/upgrade/app-roll.ts +97 -19
- package/src/upgrade/converge.ts +249 -19
- 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 +438 -46
- package/src/upgrade/upgrade.ts +64 -6
- package/src/upgrader-gcp.ts +66 -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/wizard.ts +19 -0
- package/src/wizards/gcp-cost.test.ts +103 -0
- package/src/wizards/gcp-cost.ts +125 -18
- package/src/wizards/gcp.ts +164 -22
- package/stacks/aws/Pulumi.yaml +1 -1
- package/stacks/aws/index.ts +72 -14
- package/stacks/azure/index.ts +108 -21
- package/stacks/gcp/Pulumi.yaml +1 -1
- package/stacks/gcp/README.md +1 -0
- package/stacks/gcp/index.ts +133 -24
- package/stacks/gcp-firebase/Pulumi.yaml +1 -1
- package/stacks/gcp-firebase/index.ts +212 -34
- package/stacks/self-hosted/Pulumi.yaml +3 -0
- package/stacks/self-hosted/README.md +6 -1
- package/stacks/self-hosted/index.ts +40 -5
|
@@ -7,14 +7,18 @@ import { startSelfHosted } from "./self-hosted-start.js";
|
|
|
7
7
|
const DIR = "/srv/install/.caelo";
|
|
8
8
|
|
|
9
9
|
/** A runner that records every command (minus the `docker compose -f <file>` prefix). */
|
|
10
|
-
function fakeRunner(
|
|
10
|
+
function fakeRunner(
|
|
11
|
+
fail?: (cmd: string) => boolean,
|
|
12
|
+
services = "postgres\ncaddy\ncaelo-admin\ncaelo-gateway\n",
|
|
13
|
+
): HostRunner & { calls: string[] } {
|
|
11
14
|
const calls: string[] = [];
|
|
12
15
|
return {
|
|
13
16
|
calls,
|
|
14
17
|
async run(argv): Promise<CommandResult> {
|
|
15
18
|
const cmd = argv.join(" ").replace(`docker compose -f ${DIR}/docker-compose.yml `, "");
|
|
16
19
|
calls.push(cmd);
|
|
17
|
-
|
|
20
|
+
const stdout = cmd === "config --services" ? services : "";
|
|
21
|
+
return { exitCode: fail?.(cmd) ? 1 : 0, stdout, stderr: fail?.(cmd) ? "boom" : "" };
|
|
18
22
|
},
|
|
19
23
|
};
|
|
20
24
|
}
|
|
@@ -32,13 +36,42 @@ describe("startSelfHosted (#707 — one command ends with a serving install)", (
|
|
|
32
36
|
"up -d --wait postgres",
|
|
33
37
|
`${MIGRATE} admin`,
|
|
34
38
|
`${MIGRATE} public`,
|
|
39
|
+
`${MIGRATE} public-staging`,
|
|
35
40
|
"up -d --remove-orphans",
|
|
41
|
+
"config --services",
|
|
36
42
|
"exec -T caelo-admin bun -e <probe>",
|
|
37
43
|
"exec -T caelo-gateway bun -e <probe>",
|
|
38
44
|
"exec -T caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile",
|
|
39
45
|
]);
|
|
40
46
|
});
|
|
41
47
|
|
|
48
|
+
it("#1328 — health-checks the MCP-Auth-Proxy too when the release runs one", async () => {
|
|
49
|
+
const runner = fakeRunner(undefined, "caelo-admin\ncaelo-gateway\ncaelo-mcp-auth-proxy\n");
|
|
50
|
+
const r = await startSelfHosted({ caeloDir: DIR, runner, log: () => {} });
|
|
51
|
+
expect(r).toEqual({ ok: true });
|
|
52
|
+
const probe = runner.calls.find((c) => c.startsWith("exec -T caelo-mcp-auth-proxy bun -e "));
|
|
53
|
+
expect(probe).toContain("/api/mcp/health");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("#1328 — reports an unhealthy MCP-Auth-Proxy with where to look", async () => {
|
|
57
|
+
const runner = fakeRunner(
|
|
58
|
+
(c) => c.startsWith("exec -T caelo-mcp-auth-proxy"),
|
|
59
|
+
"caelo-admin\ncaelo-gateway\ncaelo-mcp-auth-proxy\n",
|
|
60
|
+
);
|
|
61
|
+
const r = await startSelfHosted({
|
|
62
|
+
caeloDir: DIR,
|
|
63
|
+
runner,
|
|
64
|
+
log: () => {},
|
|
65
|
+
healthAttempts: 1,
|
|
66
|
+
sleep: async () => {},
|
|
67
|
+
});
|
|
68
|
+
expect(r.ok).toBe(false);
|
|
69
|
+
if (!r.ok) {
|
|
70
|
+
expect(r.stage).toBe("health");
|
|
71
|
+
expect(r.error).toContain("logs caelo-mcp-auth-proxy");
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
42
75
|
it("stops at a failed migration — nothing else is started", async () => {
|
|
43
76
|
const runner = fakeRunner((c) => c.endsWith("migrate.ts admin"));
|
|
44
77
|
const r = await startSelfHosted({ caeloDir: DIR, runner, log: () => {} });
|
package/src/self-hosted-start.ts
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* 4. start everything (`--remove-orphans` drops services an older Compose
|
|
15
15
|
* file had, e.g. the pgBackRest sidecar);
|
|
16
16
|
* 5. health-check the admin (`/_caelo/health` proves the DB pool and its
|
|
17
|
-
* role) and the
|
|
17
|
+
* role), the gateway and — when the release has one — the
|
|
18
|
+
* MCP-Auth-Proxy (#1328);
|
|
18
19
|
* 6. reload Caddy — Compose does not restart a running Caddy when only
|
|
19
20
|
* its bind-mounted Caddyfile changed, so a re-run `up` would otherwise
|
|
20
21
|
* keep serving the old vhosts.
|
|
@@ -22,8 +23,10 @@
|
|
|
22
23
|
* stopped.
|
|
23
24
|
*/
|
|
24
25
|
|
|
26
|
+
import { COMPOSE_MCP_AUTH_PROXY_SERVICE } from "./compose.js";
|
|
25
27
|
import type { HostRunner } from "./self-hosted-release.js";
|
|
26
28
|
import { compose, runMigrations, waitHealthy } from "./self-hosted-upgrade.js";
|
|
29
|
+
import type { ReleaseService } from "./upgrade/release-images.js";
|
|
27
30
|
|
|
28
31
|
export interface StartDeps {
|
|
29
32
|
readonly caeloDir: string;
|
|
@@ -71,7 +74,22 @@ export async function startSelfHosted(deps: StartDeps): Promise<StartResult> {
|
|
|
71
74
|
});
|
|
72
75
|
if (up.exitCode !== 0) return fail("start", "docker compose up failed (see above).");
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
// #1328 — the MCP-Auth-Proxy too, when the release this file runs has one
|
|
78
|
+
// (a release from before it has none; Compose knows which).
|
|
79
|
+
const listed = await runner.run(compose(caeloDir, "config", "--services"));
|
|
80
|
+
if (listed.exitCode !== 0) {
|
|
81
|
+
return fail("start", `docker compose config --services failed: ${listed.stderr.trim()}`);
|
|
82
|
+
}
|
|
83
|
+
const runsProxy = listed.stdout
|
|
84
|
+
.split("\n")
|
|
85
|
+
.map((s) => s.trim())
|
|
86
|
+
.includes(COMPOSE_MCP_AUTH_PROXY_SERVICE);
|
|
87
|
+
const services: ReleaseService[] = [
|
|
88
|
+
"admin",
|
|
89
|
+
"gateway",
|
|
90
|
+
...(runsProxy ? (["mcp-auth-proxy"] as const) : []),
|
|
91
|
+
];
|
|
92
|
+
for (const service of services) {
|
|
75
93
|
log(`Waiting for the ${service} to answer its health check …`);
|
|
76
94
|
const healthy = await waitHealthy(service, caeloDir, {
|
|
77
95
|
runner,
|
|
@@ -27,24 +27,34 @@ import {
|
|
|
27
27
|
upgradeSelfHosted,
|
|
28
28
|
} from "./self-hosted-upgrade.js";
|
|
29
29
|
|
|
30
|
+
// OLD is a release from before #1328: it has no MCP-Auth-Proxy image.
|
|
30
31
|
const OLD_DIGEST = { admin: `sha256:${"1".repeat(64)}`, gateway: `sha256:${"2".repeat(64)}` };
|
|
31
32
|
const OLD: ReleaseImages = {
|
|
32
33
|
mode: "release",
|
|
33
34
|
admin: `ghcr.io/caelo-cms/admin@${OLD_DIGEST.admin}`,
|
|
34
35
|
gateway: `ghcr.io/caelo-cms/gateway@${OLD_DIGEST.gateway}`,
|
|
35
36
|
};
|
|
36
|
-
const NEW_DIGEST = {
|
|
37
|
+
const NEW_DIGEST = {
|
|
38
|
+
admin: `sha256:${"a".repeat(64)}`,
|
|
39
|
+
gateway: `sha256:${"b".repeat(64)}`,
|
|
40
|
+
"mcp-auth-proxy": `sha256:${"c".repeat(64)}`,
|
|
41
|
+
};
|
|
37
42
|
const NEW: ReleaseImages = {
|
|
38
43
|
mode: "release",
|
|
39
44
|
admin: `ghcr.io/caelo-cms/admin@${NEW_DIGEST.admin}`,
|
|
40
45
|
gateway: `ghcr.io/caelo-cms/gateway@${NEW_DIGEST.gateway}`,
|
|
46
|
+
"mcp-auth-proxy": `ghcr.io/caelo-cms/mcp-auth-proxy@${NEW_DIGEST["mcp-auth-proxy"]}`,
|
|
41
47
|
};
|
|
42
48
|
|
|
43
49
|
/** A stand-in for generateDockerCompose: just the two image lines. */
|
|
44
50
|
const render = (images: ReleaseImages) => `admin: ${images.admin}\ngateway: ${images.gateway}\n`;
|
|
45
51
|
|
|
46
52
|
/** GHCR fake: the anonymous token dance, then `digests` per repository. */
|
|
47
|
-
function registryServing(digests: {
|
|
53
|
+
function registryServing(digests: {
|
|
54
|
+
admin: string;
|
|
55
|
+
gateway: string;
|
|
56
|
+
"mcp-auth-proxy"?: string;
|
|
57
|
+
}): typeof fetch {
|
|
48
58
|
return (async (input: string | URL | Request, init?: RequestInit) => {
|
|
49
59
|
const url = String(input);
|
|
50
60
|
if (url.startsWith("https://api.github.com/")) return Response.json({ tag_name: "v0.11.0" });
|
|
@@ -55,11 +65,14 @@ function registryServing(digests: { admin: string; gateway: string }): typeof fe
|
|
|
55
65
|
headers: { "www-authenticate": 'Bearer realm="https://ghcr.io/token",service="ghcr.io"' },
|
|
56
66
|
});
|
|
57
67
|
}
|
|
58
|
-
const service = url.includes("/caelo-cms/admin/")
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
const service = url.includes("/caelo-cms/admin/")
|
|
69
|
+
? "admin"
|
|
70
|
+
: url.includes("/caelo-cms/mcp-auth-proxy/")
|
|
71
|
+
? "mcp-auth-proxy"
|
|
72
|
+
: "gateway";
|
|
73
|
+
const digest = digests[service];
|
|
74
|
+
if (!digest) return new Response(null, { status: 404, statusText: "Not Found" });
|
|
75
|
+
return new Response(null, { status: 200, headers: { "docker-content-digest": digest } });
|
|
63
76
|
}) as typeof fetch;
|
|
64
77
|
}
|
|
65
78
|
const registry = registryServing(NEW_DIGEST);
|
|
@@ -86,17 +99,20 @@ function fakeRunner(rules: Rule[] = []): HostRunner & { calls: string[] } {
|
|
|
86
99
|
function steps(calls: readonly string[]): string[] {
|
|
87
100
|
return calls.map((c) => {
|
|
88
101
|
if (c.startsWith("cosign verify"))
|
|
89
|
-
return `verify ${c.includes("/admin@") ? "admin" : "gateway"}`;
|
|
102
|
+
return `verify ${c.includes("/admin@") ? "admin" : c.includes("/mcp-auth-proxy@") ? "mcp-auth-proxy" : "gateway"}`;
|
|
90
103
|
if (c.startsWith("cosign")) return "cosign-probe";
|
|
91
104
|
if (c.includes("pg_isready")) return "pg_isready";
|
|
92
105
|
if (c.includes("pg_dumpall")) return "dump globals";
|
|
93
106
|
if (c.includes("pg_dump ")) return `dump ${c.split(" ").at(-1)}`;
|
|
94
107
|
if (c.includes(" pull ")) return "pull";
|
|
95
108
|
if (c.includes("migrate.ts")) return `migrate ${c.split(" ").at(-1)}`;
|
|
109
|
+
if (c.includes(" up -d --no-deps caelo-admin caelo-gateway caelo-mcp-auth-proxy"))
|
|
110
|
+
return "up all";
|
|
96
111
|
if (c.includes(" up -d --no-deps caelo-admin caelo-gateway")) return "up both";
|
|
97
112
|
if (c.includes(" up -d --no-deps ")) return `up ${c.split(" ").at(-1)}`;
|
|
98
113
|
if (c.includes("/_caelo/health")) return "health admin";
|
|
99
114
|
if (c.includes("/healthz")) return "health gateway";
|
|
115
|
+
if (c.includes("/api/mcp/health")) return "health mcp-auth-proxy";
|
|
100
116
|
return c;
|
|
101
117
|
});
|
|
102
118
|
}
|
|
@@ -135,6 +151,10 @@ function deps(
|
|
|
135
151
|
runner.calls.push("converge roles");
|
|
136
152
|
return roles;
|
|
137
153
|
},
|
|
154
|
+
applyPreMigrationRoles: async () => {
|
|
155
|
+
runner.calls.push("public role limits");
|
|
156
|
+
return { ok: true } as const;
|
|
157
|
+
},
|
|
138
158
|
refreshCaddy: async () => {
|
|
139
159
|
runner.calls.push("refresh caddy");
|
|
140
160
|
return caddy;
|
|
@@ -157,25 +177,36 @@ describe("upgradeSelfHosted", () => {
|
|
|
157
177
|
"cosign-probe",
|
|
158
178
|
"verify admin",
|
|
159
179
|
"verify gateway",
|
|
180
|
+
"verify mcp-auth-proxy",
|
|
160
181
|
"pg_isready",
|
|
161
182
|
"dump globals",
|
|
162
183
|
"dump cms_admin",
|
|
163
184
|
"dump cms_public",
|
|
164
185
|
"pull",
|
|
186
|
+
"up postgres",
|
|
187
|
+
"public role limits",
|
|
165
188
|
"migrate admin",
|
|
166
189
|
"migrate public",
|
|
190
|
+
"migrate public-staging",
|
|
167
191
|
"converge roles",
|
|
168
192
|
"up caelo-admin",
|
|
169
193
|
"health admin",
|
|
170
194
|
"up caelo-gateway",
|
|
171
195
|
"health gateway",
|
|
196
|
+
// #1328 — the upgrade from a release without the proxy creates it.
|
|
197
|
+
"up caelo-mcp-auth-proxy",
|
|
198
|
+
"health mcp-auth-proxy",
|
|
172
199
|
"refresh caddy",
|
|
173
200
|
]);
|
|
174
201
|
// The migrations run the NEW admin image: the Compose file already names it.
|
|
175
202
|
expect(readFileSync(composePath(dir), "utf8")).toBe(render(NEW));
|
|
176
203
|
const release = readRelease(dir);
|
|
177
204
|
expect(release?.version).toBe("0.11.0");
|
|
178
|
-
expect(release?.images).toEqual({
|
|
205
|
+
expect(release?.images).toEqual({
|
|
206
|
+
admin: NEW.admin,
|
|
207
|
+
gateway: NEW.gateway,
|
|
208
|
+
"mcp-auth-proxy": NEW["mcp-auth-proxy"],
|
|
209
|
+
});
|
|
179
210
|
expect(release?.history.at(-1)).toMatchObject({
|
|
180
211
|
from: "0.10.0",
|
|
181
212
|
to: "0.11.0",
|
|
@@ -206,9 +237,11 @@ describe("upgradeSelfHosted", () => {
|
|
|
206
237
|
"cosign-probe",
|
|
207
238
|
"verify admin",
|
|
208
239
|
"verify gateway",
|
|
209
|
-
"
|
|
240
|
+
"verify mcp-auth-proxy",
|
|
241
|
+
"up all",
|
|
210
242
|
"health admin",
|
|
211
243
|
"health gateway",
|
|
244
|
+
"health mcp-auth-proxy",
|
|
212
245
|
"refresh caddy",
|
|
213
246
|
]);
|
|
214
247
|
});
|
|
@@ -257,7 +290,13 @@ describe("upgradeSelfHosted", () => {
|
|
|
257
290
|
{ version: "0.10.0" },
|
|
258
291
|
{
|
|
259
292
|
...deps(runner),
|
|
260
|
-
|
|
293
|
+
// The target release ships a proxy image; the fake render prints
|
|
294
|
+
// only admin + gateway, so the file still reads as OLD.
|
|
295
|
+
fetchImpl: registryServing({
|
|
296
|
+
admin: OLD_DIGEST.admin,
|
|
297
|
+
gateway: OLD_DIGEST.gateway,
|
|
298
|
+
"mcp-auth-proxy": NEW_DIGEST["mcp-auth-proxy"],
|
|
299
|
+
}),
|
|
261
300
|
},
|
|
262
301
|
);
|
|
263
302
|
expect(r).toEqual({ ok: true, status: "upgraded", version: "0.10.0" });
|
|
@@ -305,7 +344,9 @@ describe("upgradeSelfHosted", () => {
|
|
|
305
344
|
]);
|
|
306
345
|
const r = await upgradeSelfHosted({ version: "0.11.0" }, deps(runner));
|
|
307
346
|
expect(r).toMatchObject({ ok: false, stage: "migrate", previousRunning: true });
|
|
308
|
-
|
|
347
|
+
// Postgres moved onto this release's TLS config before the migrations
|
|
348
|
+
// (it serves the previous release too); no Caelo service restarted.
|
|
349
|
+
expect(steps(runner.calls).some((s) => s.startsWith("up caelo"))).toBe(false);
|
|
309
350
|
expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
|
|
310
351
|
});
|
|
311
352
|
|
|
@@ -325,7 +366,9 @@ describe("upgradeSelfHosted", () => {
|
|
|
325
366
|
);
|
|
326
367
|
expect(r).toMatchObject({ ok: false, stage: "roles", previousRunning: true });
|
|
327
368
|
if (!r.ok) expect(r.error).toContain("role is busy");
|
|
328
|
-
|
|
369
|
+
// Postgres moved onto this release's TLS config before the migrations
|
|
370
|
+
// (it serves the previous release too); no Caelo service restarted.
|
|
371
|
+
expect(steps(runner.calls).some((x) => x.startsWith("up caelo"))).toBe(false);
|
|
329
372
|
expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
|
|
330
373
|
expect(readRelease(dir)?.version).toBe("0.10.0");
|
|
331
374
|
});
|
|
@@ -359,6 +402,23 @@ describe("upgradeSelfHosted", () => {
|
|
|
359
402
|
expect(release?.history.at(-1)).toMatchObject({ to: "0.11.0", outcome: "rolled-back" });
|
|
360
403
|
});
|
|
361
404
|
|
|
405
|
+
it("rolls back to a release without the proxy when the new MCP-Auth-Proxy is unhealthy (#1328)", async () => {
|
|
406
|
+
const runner = fakeRunner([
|
|
407
|
+
(c) => (c.includes("/api/mcp/health") ? { exitCode: 1 } : undefined),
|
|
408
|
+
]);
|
|
409
|
+
const r = await upgradeSelfHosted({ version: "0.11.0" }, deps(runner));
|
|
410
|
+
expect(r).toMatchObject({ ok: false, stage: "roll-mcp-auth-proxy", previousRunning: true });
|
|
411
|
+
const s = steps(runner.calls);
|
|
412
|
+
// OLD predates the proxy: the rollback starts and checks only admin + gateway.
|
|
413
|
+
expect(s.slice(s.lastIndexOf("up both"))).toEqual([
|
|
414
|
+
"up both",
|
|
415
|
+
"health admin",
|
|
416
|
+
"health gateway",
|
|
417
|
+
]);
|
|
418
|
+
expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
|
|
419
|
+
expect(readRelease(dir)?.images["mcp-auth-proxy"]).toBeUndefined();
|
|
420
|
+
});
|
|
421
|
+
|
|
362
422
|
it("reports a failed rollback loudly", async () => {
|
|
363
423
|
const runner = fakeRunner([
|
|
364
424
|
(c) => (c.includes("/_caelo/health") ? { exitCode: 1 } : undefined),
|
|
@@ -429,10 +489,38 @@ describe("upgradeSelfHosted — driven by the host agent (#696 Phase 4c)", () =>
|
|
|
429
489
|
);
|
|
430
490
|
expect(r).toMatchObject({ ok: false, stage: "verify", previousRunning: true });
|
|
431
491
|
expect(r.ok ? "" : r.error).toContain("gateway");
|
|
432
|
-
expect(steps(runner.calls)).toEqual([
|
|
492
|
+
expect(steps(runner.calls)).toEqual([
|
|
493
|
+
"cosign-probe",
|
|
494
|
+
"verify admin",
|
|
495
|
+
"verify gateway",
|
|
496
|
+
"verify mcp-auth-proxy",
|
|
497
|
+
]);
|
|
433
498
|
expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
|
|
434
499
|
});
|
|
435
500
|
|
|
501
|
+
it("refuses a registry that serves another MCP-Auth-Proxy than the manifest names (#1328)", async () => {
|
|
502
|
+
const r = await upgradeSelfHosted(
|
|
503
|
+
{
|
|
504
|
+
version: "0.11.0",
|
|
505
|
+
expectedDigests: { ...NEW_DIGEST, "mcp-auth-proxy": `sha256:${"d".repeat(64)}` },
|
|
506
|
+
},
|
|
507
|
+
deps(fakeRunner()),
|
|
508
|
+
);
|
|
509
|
+
expect(r).toMatchObject({ ok: false, stage: "verify", previousRunning: true });
|
|
510
|
+
expect(r.ok ? "" : r.error).toContain("mcp-auth-proxy");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("checks only admin + gateway for an agent that names no proxy digest", async () => {
|
|
514
|
+
const r = await upgradeSelfHosted(
|
|
515
|
+
{
|
|
516
|
+
version: "0.11.0",
|
|
517
|
+
expectedDigests: { admin: NEW_DIGEST.admin, gateway: NEW_DIGEST.gateway },
|
|
518
|
+
},
|
|
519
|
+
deps(fakeRunner()),
|
|
520
|
+
);
|
|
521
|
+
expect(r.ok).toBe(true);
|
|
522
|
+
});
|
|
523
|
+
|
|
436
524
|
it("names its stages for the admin's timeline, in order", async () => {
|
|
437
525
|
const phases: string[] = [];
|
|
438
526
|
const r = await upgradeSelfHosted(
|
|
@@ -449,6 +537,8 @@ describe("upgradeSelfHosted — driven by the host agent (#696 Phase 4c)", () =>
|
|
|
449
537
|
"health",
|
|
450
538
|
"roll",
|
|
451
539
|
"health",
|
|
540
|
+
"roll",
|
|
541
|
+
"health",
|
|
452
542
|
"record",
|
|
453
543
|
]);
|
|
454
544
|
});
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* 2. back up both databases (pg_dump, hard-fail) — the last moment the
|
|
10
10
|
* schema is guaranteed to match the running images;
|
|
11
11
|
* 3. write the new digests into the Compose file, `docker compose pull`;
|
|
12
|
+
* 3b. #1338 — recreate Postgres when the Compose file changed it (TLS with
|
|
13
|
+
* the install's own certificate, self-hosted-db-tls.ts: the new images
|
|
14
|
+
* connect with verify-full only; the old containers keep connecting,
|
|
15
|
+
* Postgres still accepts them) and, as the superuser, create the
|
|
16
|
+
* staging database with its gateway login (#1329) and apply both gateway
|
|
17
|
+
* logins' limits as the
|
|
18
|
+
* superuser (the migrations' role audit refuses public_role without
|
|
19
|
+
* them, and the self-hosted admin_role cannot set them);
|
|
12
20
|
* 4. run the migrations from the NEW admin image (`docker compose run`,
|
|
13
21
|
* the same `migrate.ts` entrypoint the GCP migrate job runs) while the
|
|
14
22
|
* old containers keep serving — migrations are expand-only, so the
|
|
@@ -20,7 +28,9 @@
|
|
|
20
28
|
* refuses an install that skipped the release the old role's last user
|
|
21
29
|
* was retired in (run-order.ts): past it, nothing running uses it;
|
|
22
30
|
* 6. recreate the admin, health-check it (`/_caelo/health`, which proves
|
|
23
|
-
* the DB pool and roles); then the gateway (`/healthz`);
|
|
31
|
+
* the DB pool and roles); then the gateway (`/healthz`); then the
|
|
32
|
+
* MCP-Auth-Proxy (`/api/mcp/health`, #1328) — on the upgrade that first
|
|
33
|
+
* brings it, this creates its container;
|
|
24
34
|
* 7. re-write the Caddyfile and reload Caddy — the edge headers live there
|
|
25
35
|
* (#891), and only the new release's generator knows them;
|
|
26
36
|
* 8. record the release in `.caelo/release.json`.
|
|
@@ -50,7 +60,14 @@ import {
|
|
|
50
60
|
} from "node:fs";
|
|
51
61
|
import { join } from "node:path";
|
|
52
62
|
import type { PlatformUpdatePhase } from "@caelo-cms/shared";
|
|
53
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
COMPOSE_MCP_AUTH_PROXY_PORT,
|
|
65
|
+
COMPOSE_MCP_AUTH_PROXY_SERVICE,
|
|
66
|
+
ghcrImageDigest,
|
|
67
|
+
MEDIA_DIR,
|
|
68
|
+
type ReleaseImages,
|
|
69
|
+
} from "./compose.js";
|
|
70
|
+
import { MIGRATION_TARGETS } from "./migration-targets.js";
|
|
54
71
|
import { selfCommand } from "./self-command.js";
|
|
55
72
|
import {
|
|
56
73
|
type HostRunner,
|
|
@@ -61,10 +78,28 @@ import {
|
|
|
61
78
|
resolveVerifiedRelease,
|
|
62
79
|
} from "./self-hosted-release.js";
|
|
63
80
|
import type { ConvergeOutcome } from "./self-hosted-roles.js";
|
|
64
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
fetchLatestReleaseTag,
|
|
83
|
+
RELEASE_SERVICES,
|
|
84
|
+
type ReleaseService,
|
|
85
|
+
releaseTag,
|
|
86
|
+
} from "./upgrade/release-images.js";
|
|
87
|
+
|
|
88
|
+
/** The Compose service names of the Caelo services. */
|
|
89
|
+
const COMPOSE_SERVICE: Readonly<Record<ReleaseService, string>> = {
|
|
90
|
+
admin: "caelo-admin",
|
|
91
|
+
gateway: "caelo-gateway",
|
|
92
|
+
"mcp-auth-proxy": COMPOSE_MCP_AUTH_PROXY_SERVICE,
|
|
93
|
+
};
|
|
65
94
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
95
|
+
/**
|
|
96
|
+
* The release services a Compose file for `images` runs, in roll order: all
|
|
97
|
+
* of them, except the MCP-Auth-Proxy for a release recorded before it
|
|
98
|
+
* shipped (#1328, compose.ts ReleaseImages).
|
|
99
|
+
*/
|
|
100
|
+
function servicesOf(images: ReleaseImages): ReleaseService[] {
|
|
101
|
+
return RELEASE_SERVICES.filter((s) => images[s] !== undefined);
|
|
102
|
+
}
|
|
68
103
|
|
|
69
104
|
/** Progress output; the CLI wires it to the terminal, tests to an array. */
|
|
70
105
|
export interface UpgradeLog {
|
|
@@ -92,6 +127,14 @@ export interface SelfHostedUpgradeDeps {
|
|
|
92
127
|
* (`convergeSelfHostedRoles`); drops roles a release retired (#635).
|
|
93
128
|
*/
|
|
94
129
|
readonly convergeRoles: () => Promise<ConvergeOutcome>;
|
|
130
|
+
/**
|
|
131
|
+
* #1338 / #1329 — as the Postgres superuser (`applyPreMigrationRoles`):
|
|
132
|
+
* creates the staging database and its gateway login on an install from
|
|
133
|
+
* before them, and applies both gateway logins' limits, before the
|
|
134
|
+
* migrations that run on that database and whose role audit
|
|
135
|
+
* checks them.
|
|
136
|
+
*/
|
|
137
|
+
readonly applyPreMigrationRoles: () => Promise<ConvergeOutcome>;
|
|
95
138
|
/**
|
|
96
139
|
* Re-writes the Caddyfile from the install's config + domains table and
|
|
97
140
|
* reloads Caddy (#891): the edge headers (the SVG CSP, nosniff) live in
|
|
@@ -112,12 +155,17 @@ export interface SelfHostedUpgradeOpts {
|
|
|
112
155
|
readonly channel?: "stable" | "rc" | "beta";
|
|
113
156
|
readonly skipVerify?: boolean;
|
|
114
157
|
/**
|
|
115
|
-
* The
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
158
|
+
* The digests the release's signed manifest names (the host agent passes
|
|
159
|
+
* them). The digests GHCR serves for the tag must match, or nothing
|
|
160
|
+
* changes — a registry cannot swap in another signed image of an older
|
|
161
|
+
* release under the new tag. The MCP-Auth-Proxy's is compared whenever it
|
|
162
|
+
* is given (#1328; an agent from before it passes only admin + gateway).
|
|
119
163
|
*/
|
|
120
|
-
readonly expectedDigests?: {
|
|
164
|
+
readonly expectedDigests?: {
|
|
165
|
+
readonly admin: string;
|
|
166
|
+
readonly gateway: string;
|
|
167
|
+
readonly "mcp-auth-proxy"?: string;
|
|
168
|
+
};
|
|
121
169
|
}
|
|
122
170
|
|
|
123
171
|
export type SelfHostedUpgradeResult =
|
|
@@ -140,6 +188,7 @@ type UpgradeStage =
|
|
|
140
188
|
| "roles"
|
|
141
189
|
| "roll-admin"
|
|
142
190
|
| "roll-gateway"
|
|
191
|
+
| "roll-mcp-auth-proxy"
|
|
143
192
|
| "caddy";
|
|
144
193
|
|
|
145
194
|
/** Where an install from before #696 keeps its original Compose file during the move. */
|
|
@@ -247,6 +296,10 @@ export async function upgradeSelfHosted(
|
|
|
247
296
|
const previousCompose = current
|
|
248
297
|
? deps.renderCompose(releaseImages(current))
|
|
249
298
|
: legacyComposeCheckpoint(caeloDir, live);
|
|
299
|
+
// The legacy (repo-mounted) file predates the MCP-Auth-Proxy.
|
|
300
|
+
const previousServices: readonly ReleaseService[] = current
|
|
301
|
+
? servicesOf(releaseImages(current))
|
|
302
|
+
: ["admin", "gateway"];
|
|
250
303
|
|
|
251
304
|
// 1. Target release → verified digests.
|
|
252
305
|
log.phase?.("verify");
|
|
@@ -265,8 +318,9 @@ export async function upgradeSelfHosted(
|
|
|
265
318
|
if (opts.skipVerify) log.warn("--skip-verify set — image signatures NOT verified.");
|
|
266
319
|
const images = resolved.images;
|
|
267
320
|
if (opts.expectedDigests) {
|
|
321
|
+
const expected = opts.expectedDigests;
|
|
268
322
|
const mismatch = RELEASE_SERVICES.filter(
|
|
269
|
-
(s) => ghcrImageDigest(s, images[s]) !==
|
|
323
|
+
(s) => expected[s] !== undefined && ghcrImageDigest(s, images[s] ?? "") !== expected[s],
|
|
270
324
|
);
|
|
271
325
|
if (mismatch.length > 0) {
|
|
272
326
|
return fail(
|
|
@@ -278,8 +332,7 @@ export async function upgradeSelfHosted(
|
|
|
278
332
|
}
|
|
279
333
|
if (
|
|
280
334
|
current &&
|
|
281
|
-
current.images
|
|
282
|
-
current.images.gateway === images.gateway &&
|
|
335
|
+
RELEASE_SERVICES.every((s) => current.images[s] === images[s]) &&
|
|
283
336
|
live === previousCompose
|
|
284
337
|
) {
|
|
285
338
|
// Recorded, and the Compose file agrees. `up` converges containers a
|
|
@@ -289,7 +342,7 @@ export async function upgradeSelfHosted(
|
|
|
289
342
|
log.info(
|
|
290
343
|
`Already on ${current.version}; checking the running containers match and are healthy.`,
|
|
291
344
|
);
|
|
292
|
-
const converged = await
|
|
345
|
+
const converged = await startAll(deps, servicesOf(images));
|
|
293
346
|
if (!converged.ok) {
|
|
294
347
|
return fail(
|
|
295
348
|
"converge",
|
|
@@ -328,13 +381,41 @@ export async function upgradeSelfHosted(
|
|
|
328
381
|
log.phase?.("roll");
|
|
329
382
|
log.step("Pulling the new images");
|
|
330
383
|
const pull = await runner.run(
|
|
331
|
-
compose(caeloDir, "pull",
|
|
384
|
+
compose(caeloDir, "pull", ...servicesOf(images).map((s) => COMPOSE_SERVICE[s])),
|
|
332
385
|
);
|
|
333
386
|
if (pull.exitCode !== 0) {
|
|
334
387
|
restoreCompose(file, previousCompose);
|
|
335
388
|
return fail("pull", `docker compose pull failed: ${tail(pull.stderr)}`, true);
|
|
336
389
|
}
|
|
337
390
|
|
|
391
|
+
// 3b. Postgres on this release's config (TLS, #1338). `up -d` recreates
|
|
392
|
+
// it only when its definition changed; `--wait` returns once it is
|
|
393
|
+
// healthy. The data volume stays; the old admin and gateway reconnect.
|
|
394
|
+
log.step("Bringing Postgres onto this release's configuration (TLS)");
|
|
395
|
+
const pg = await runner.run(compose(caeloDir, "up", "-d", "--no-deps", "--wait", "postgres"));
|
|
396
|
+
if (pg.exitCode !== 0) {
|
|
397
|
+
restoreCompose(file, previousCompose);
|
|
398
|
+
const back = await runner.run(compose(caeloDir, "up", "-d", "--no-deps", "--wait", "postgres"));
|
|
399
|
+
return fail(
|
|
400
|
+
"pull",
|
|
401
|
+
`Postgres did not come up on the new configuration: ${tail(pg.stderr)}\n${
|
|
402
|
+
back.exitCode === 0
|
|
403
|
+
? "Postgres runs on the previous configuration again; nothing else changed."
|
|
404
|
+
: `Postgres did not come back on the previous configuration either: ${tail(back.stderr)}. Check \`docker compose ps\` / \`docker compose logs postgres\`.`
|
|
405
|
+
}`,
|
|
406
|
+
back.exitCode === 0,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
const limits = await deps.applyPreMigrationRoles();
|
|
410
|
+
if (!limits.ok) {
|
|
411
|
+
restoreCompose(file, previousCompose);
|
|
412
|
+
return fail(
|
|
413
|
+
"roles",
|
|
414
|
+
`Could not prepare the database roles for the migrations (the staging database cms_public_staging and the gateway logins' connection and statement limits): ${limits.error}\nThe previous release keeps running; no migration ran. Database backup: ${backup.dir}`,
|
|
415
|
+
true,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
338
419
|
// 4. Migrations from the new admin image; the old containers keep serving.
|
|
339
420
|
log.phase?.("migrations");
|
|
340
421
|
log.step("Running database migrations with the new admin image");
|
|
@@ -360,11 +441,11 @@ export async function upgradeSelfHosted(
|
|
|
360
441
|
);
|
|
361
442
|
}
|
|
362
443
|
|
|
363
|
-
// 6. Roll admin, then gateway, each health-checked.
|
|
364
|
-
for (const service of
|
|
365
|
-
const stage = service
|
|
444
|
+
// 6. Roll admin, then gateway, then the MCP-Auth-Proxy, each health-checked.
|
|
445
|
+
for (const service of servicesOf(images)) {
|
|
446
|
+
const stage: UpgradeStage = `roll-${service}`;
|
|
366
447
|
log.phase?.("roll");
|
|
367
|
-
log.step(`Restarting ${service} on ${shortDigest(images[service])}`);
|
|
448
|
+
log.step(`Restarting ${service} on ${shortDigest(images[service] ?? "")}`);
|
|
368
449
|
const up = await runner.run(
|
|
369
450
|
compose(caeloDir, "up", "-d", "--no-deps", COMPOSE_SERVICE[service]),
|
|
370
451
|
);
|
|
@@ -375,7 +456,7 @@ export async function upgradeSelfHosted(
|
|
|
375
456
|
: { ok: false as const, error: `docker compose up failed: ${tail(up.stderr)}` };
|
|
376
457
|
if (!healthy.ok) {
|
|
377
458
|
log.error(`${service} is not healthy on ${target.version}: ${healthy.error}`);
|
|
378
|
-
const restored = await rollback(file, previousCompose, deps);
|
|
459
|
+
const restored = await rollback(file, previousCompose, previousServices, deps);
|
|
379
460
|
if (current) {
|
|
380
461
|
recordRollback(caeloDir, current, target.version, `${stage}: ${healthy.error}`, now());
|
|
381
462
|
}
|
|
@@ -524,7 +605,8 @@ export async function dumpDatabases(
|
|
|
524
605
|
}
|
|
525
606
|
|
|
526
607
|
/**
|
|
527
|
-
* Apply the committed migrations (cms_admin, then cms_public
|
|
608
|
+
* Apply the committed migrations (cms_admin, then cms_public and its staging
|
|
609
|
+
* twin cms_public_staging, #1329) with the
|
|
528
610
|
* admin image the Compose file names, as a one-off container — the
|
|
529
611
|
* self-hosted equivalent of the GCP `caelo-migrate-<target>` Cloud Run job.
|
|
530
612
|
* drizzle's bookkeeping makes re-runs apply only new migrations.
|
|
@@ -533,7 +615,7 @@ export async function runMigrations(
|
|
|
533
615
|
caeloDir: string,
|
|
534
616
|
runner: HostRunner,
|
|
535
617
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
536
|
-
for (const target of
|
|
618
|
+
for (const target of MIGRATION_TARGETS) {
|
|
537
619
|
const r = await runner.run(
|
|
538
620
|
compose(
|
|
539
621
|
caeloDir,
|
|
@@ -559,13 +641,16 @@ export async function runMigrations(
|
|
|
559
641
|
* In-container probe scripts (`bun -e`): the containers publish no host
|
|
560
642
|
* ports, so the probe runs where the service listens. The admin's
|
|
561
643
|
* `/_caelo/health` checks the DB pool + roles; the gateway's `/healthz` is
|
|
562
|
-
* its liveness endpoint
|
|
644
|
+
* its liveness endpoint; the MCP-Auth-Proxy's `/api/mcp/health` answers
|
|
645
|
+
* even while remote MCP is unavailable (#1328). Its image has no shell, so
|
|
646
|
+
* the probe is `bun -e`, never `sh -c`.
|
|
563
647
|
*/
|
|
564
|
-
const HEALTH_PROBE: Record<
|
|
648
|
+
const HEALTH_PROBE: Record<ReleaseService, string> = {
|
|
565
649
|
admin:
|
|
566
650
|
'const r=await fetch("http://127.0.0.1:"+(process.env.PORT||"3000")+"/_caelo/health").catch(()=>null);process.exit(r&&r.ok?0:1)',
|
|
567
651
|
gateway:
|
|
568
652
|
'const r=await fetch("http://127.0.0.1:"+(process.env.PORT||process.env.GATEWAY_PORT||"8090")+"/healthz").catch(()=>null);process.exit(r&&r.ok?0:1)',
|
|
653
|
+
"mcp-auth-proxy": `const r=await fetch("http://127.0.0.1:"+(process.env.MCP_AUTH_PROXY_PORT||"${COMPOSE_MCP_AUTH_PROXY_PORT}")+"/api/mcp/health").catch(()=>null);process.exit(r&&r.ok?0:1)`,
|
|
569
654
|
};
|
|
570
655
|
|
|
571
656
|
/**
|
|
@@ -573,7 +658,7 @@ const HEALTH_PROBE: Record<"admin" | "gateway", string> = {
|
|
|
573
658
|
* after `healthAttempts` tries (default 45, 2 s apart).
|
|
574
659
|
*/
|
|
575
660
|
export async function waitHealthy(
|
|
576
|
-
service:
|
|
661
|
+
service: ReleaseService,
|
|
577
662
|
caeloDir: string,
|
|
578
663
|
deps: Pick<SelfHostedUpgradeDeps, "runner" | "sleep" | "healthAttempts" | "healthIntervalMs">,
|
|
579
664
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
@@ -595,28 +680,35 @@ export async function waitHealthy(
|
|
|
595
680
|
};
|
|
596
681
|
}
|
|
597
682
|
|
|
598
|
-
/**
|
|
683
|
+
/**
|
|
684
|
+
* Put the previous Compose file back, restart its services on it,
|
|
685
|
+
* health-check them. `previousServices` are the services that file runs (a
|
|
686
|
+
* release from before #1328 has no MCP-Auth-Proxy; a proxy container the
|
|
687
|
+
* failed run created then keeps running unused until the next upgrade).
|
|
688
|
+
*/
|
|
599
689
|
async function rollback(
|
|
600
690
|
file: string,
|
|
601
691
|
previousCompose: string,
|
|
692
|
+
previousServices: readonly ReleaseService[],
|
|
602
693
|
deps: SelfHostedUpgradeDeps,
|
|
603
694
|
): Promise<boolean> {
|
|
604
695
|
deps.log.phase?.("rollback");
|
|
605
696
|
deps.log.warn("Rolling back to the previous images");
|
|
606
697
|
restoreCompose(file, previousCompose);
|
|
607
|
-
return (await
|
|
698
|
+
return (await startAll(deps, previousServices)).ok;
|
|
608
699
|
}
|
|
609
700
|
|
|
610
|
-
/** `up -d`
|
|
611
|
-
async function
|
|
701
|
+
/** `up -d` `services` on the current Compose file, then health-check each. */
|
|
702
|
+
async function startAll(
|
|
612
703
|
deps: SelfHostedUpgradeDeps,
|
|
704
|
+
services: readonly ReleaseService[],
|
|
613
705
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
614
706
|
const up = await deps.runner.run(
|
|
615
|
-
compose(deps.caeloDir, "up", "-d", "--no-deps",
|
|
707
|
+
compose(deps.caeloDir, "up", "-d", "--no-deps", ...services.map((s) => COMPOSE_SERVICE[s])),
|
|
616
708
|
);
|
|
617
709
|
if (up.exitCode !== 0)
|
|
618
710
|
return { ok: false, error: `docker compose up failed: ${tail(up.stderr)}` };
|
|
619
|
-
for (const service of
|
|
711
|
+
for (const service of services) {
|
|
620
712
|
const healthy = await waitHealthy(service, deps.caeloDir, deps);
|
|
621
713
|
if (!healthy.ok) return { ok: false, error: `${service}: ${healthy.error}` };
|
|
622
714
|
}
|