@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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #1338 — every database URL a provider adapter builds asks for
|
|
5
|
+
* `sslmode=verify-full`, and no adapter still builds a weaker one
|
|
6
|
+
* (CLAUDE.md §2 no fallbacks: a downgrade is never silent).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { databaseTlsProblem } from "@caelo-cms/shared";
|
|
13
|
+
import { generateDockerCompose } from "./compose.js";
|
|
14
|
+
import { databaseUrls } from "./stack-contract.js";
|
|
15
|
+
|
|
16
|
+
const DNS = "abc123.p-label.europe-west1.sql-psa.goog";
|
|
17
|
+
|
|
18
|
+
describe("database URL builders (#1338)", () => {
|
|
19
|
+
it("Cloud SQL: verify-full on the instance's DNS name, never on its IP", () => {
|
|
20
|
+
for (const url of Object.values(databaseUrls(DNS))) {
|
|
21
|
+
expect(databaseTlsProblem(url)).toBeNull();
|
|
22
|
+
expect(new URL(url).hostname).toBe(DNS);
|
|
23
|
+
}
|
|
24
|
+
expect(() => databaseUrls("10.20.0.3")).toThrow(/DNS name/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("self-hosted Compose: every service URL is verify-full against the install's CA", () => {
|
|
28
|
+
const yaml = generateDockerCompose({
|
|
29
|
+
domain: "example.com",
|
|
30
|
+
postgresPassword: "superpw",
|
|
31
|
+
rolePasswords: { admin: "adminpw", public: "publicpw" },
|
|
32
|
+
caeloSecretKek: "0".repeat(64),
|
|
33
|
+
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
34
|
+
internalSecret: "i".repeat(48),
|
|
35
|
+
diskSize: "20Gi",
|
|
36
|
+
images: {
|
|
37
|
+
mode: "release",
|
|
38
|
+
admin: `ghcr.io/caelo-cms/admin@sha256:${"a".repeat(64)}`,
|
|
39
|
+
gateway: `ghcr.io/caelo-cms/gateway@sha256:${"b".repeat(64)}`,
|
|
40
|
+
},
|
|
41
|
+
ports: { http: 80, https: 443 },
|
|
42
|
+
project: "caelo-example-com",
|
|
43
|
+
});
|
|
44
|
+
const urls = [...yaml.matchAll(/_DATABASE_URL: "([^"]+)"/g)].map((m) => m[1] as string);
|
|
45
|
+
expect(urls).toHaveLength(3);
|
|
46
|
+
for (const url of urls) expect(databaseTlsProblem(url)).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
for (const stack of ["gcp", "gcp-firebase", "aws", "azure"]) {
|
|
50
|
+
it(`stacks/${stack} builds no URL weaker than verify-full`, () => {
|
|
51
|
+
const src = readFileSync(resolve(import.meta.dir, `../stacks/${stack}/index.ts`), "utf8");
|
|
52
|
+
expect(src).not.toMatch(/sslmode=(disable|allow|prefer|require|verify-ca)\b/);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
@@ -29,6 +29,17 @@ function fakeGcloud(answers: Record<string, GcloudResult[]>) {
|
|
|
29
29
|
return { run, calls };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* A gcp-firebase install since #1328: the admin, the data gateway and the
|
|
34
|
+
* MCP-Auth-Proxy — the service that was the gateway (E13 a).
|
|
35
|
+
*/
|
|
36
|
+
const FIREBASE = { provider: "gcp-firebase" as const, projectId: "p", region: "europe-west1" };
|
|
37
|
+
const firebaseLists = () => ({
|
|
38
|
+
"metadata.name~^caelo-production-admin": [ok("caelo-production-admin-abc\n")],
|
|
39
|
+
"metadata.name~^caelo-production-data-gateway": [ok("caelo-production-data-gateway\n")],
|
|
40
|
+
"metadata.name~^caelo-production-gateway": [ok("caelo-production-gateway-def\n")],
|
|
41
|
+
});
|
|
42
|
+
|
|
32
43
|
describe("digestFromImageRef", () => {
|
|
33
44
|
it("extracts the pinned digest", () => {
|
|
34
45
|
expect(digestFromImageRef(`${REG}/admin@${A}\n`)).toBe(A);
|
|
@@ -39,16 +50,43 @@ describe("digestFromImageRef", () => {
|
|
|
39
50
|
});
|
|
40
51
|
|
|
41
52
|
describe("readDeployedImageDigests", () => {
|
|
42
|
-
it("reads
|
|
53
|
+
it("reads the three services' digests, each from the service playing its role", async () => {
|
|
43
54
|
const { run, calls } = fakeGcloud({
|
|
55
|
+
...firebaseLists(),
|
|
56
|
+
"describe caelo-production-admin-abc": [ok(`${REG}/admin@${A}`)],
|
|
57
|
+
"describe caelo-production-data-gateway": [ok(`${REG}/gateway@${B}`)],
|
|
58
|
+
"describe caelo-production-gateway-def": [ok(`${REG}/mcp-auth-proxy@${C}`)],
|
|
59
|
+
});
|
|
60
|
+
const r = await readDeployedImageDigests({ ...FIREBASE, run });
|
|
61
|
+
expect(r).toEqual({ ok: true, digests: { admin: A, gateway: B, "mcp-auth-proxy": C } });
|
|
62
|
+
expect(calls.every((c) => c.includes("--project=p"))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("finds the gcp proxy under its own name", async () => {
|
|
66
|
+
const { run } = fakeGcloud({
|
|
44
67
|
"metadata.name~^caelo-production-admin": [ok("caelo-production-admin-abc\n")],
|
|
45
68
|
"metadata.name~^caelo-production-gateway": [ok("caelo-production-gateway-def\n")],
|
|
69
|
+
"metadata.name~^caelo-production-mcp-auth-proxy": [ok("caelo-production-mcp-auth-proxy\n")],
|
|
46
70
|
"describe caelo-production-admin-abc": [ok(`${REG}/admin@${A}`)],
|
|
47
71
|
"describe caelo-production-gateway-def": [ok(`${REG}/gateway@${B}`)],
|
|
72
|
+
"describe caelo-production-mcp-auth-proxy": [ok(`${REG}/mcp-auth-proxy@${C}`)],
|
|
48
73
|
});
|
|
49
|
-
const r = await readDeployedImageDigests({
|
|
50
|
-
expect(r).toEqual({ ok: true, digests: { admin: A, gateway: B } });
|
|
51
|
-
|
|
74
|
+
const r = await readDeployedImageDigests({ ...FIREBASE, provider: "gcp", run });
|
|
75
|
+
expect(r).toEqual({ ok: true, digests: { admin: A, gateway: B, "mcp-auth-proxy": C } });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("sends an install without its #1328 services to upgrade", async () => {
|
|
79
|
+
const { run } = fakeGcloud({
|
|
80
|
+
"metadata.name~^caelo-production-admin": [ok("caelo-production-admin-abc\n")],
|
|
81
|
+
"metadata.name~^caelo-production-data-gateway": [ok("")],
|
|
82
|
+
"describe caelo-production-admin-abc": [ok(`${REG}/admin@${A}`)],
|
|
83
|
+
});
|
|
84
|
+
const r = await readDeployedImageDigests({ ...FIREBASE, run });
|
|
85
|
+
expect(r.ok).toBe(false);
|
|
86
|
+
if (!r.ok) {
|
|
87
|
+
expect(r.error).toContain("no caelo-production-data-gateway* Cloud Run service");
|
|
88
|
+
expect(r.error).toContain("Run `upgrade` first");
|
|
89
|
+
}
|
|
52
90
|
});
|
|
53
91
|
|
|
54
92
|
it("refuses an ambiguous service match", async () => {
|
|
@@ -57,7 +95,7 @@ describe("readDeployedImageDigests", () => {
|
|
|
57
95
|
ok("caelo-production-admin-a\ncaelo-production-admin-b\n"),
|
|
58
96
|
],
|
|
59
97
|
});
|
|
60
|
-
const r = await readDeployedImageDigests({
|
|
98
|
+
const r = await readDeployedImageDigests({ ...FIREBASE, run });
|
|
61
99
|
expect(r.ok).toBe(false);
|
|
62
100
|
if (!r.ok)
|
|
63
101
|
expect(r.error).toContain("found caelo-production-admin-a, caelo-production-admin-b");
|
|
@@ -68,14 +106,14 @@ describe("readDeployedImageDigests", () => {
|
|
|
68
106
|
"metadata.name~^caelo-production-admin": [ok("caelo-production-admin-abc\n")],
|
|
69
107
|
"describe caelo-production-admin-abc": [ok(`${REG}/admin:latest`)],
|
|
70
108
|
});
|
|
71
|
-
const r = await readDeployedImageDigests({
|
|
109
|
+
const r = await readDeployedImageDigests({ ...FIREBASE, run });
|
|
72
110
|
expect(r.ok).toBe(false);
|
|
73
111
|
if (!r.ok) expect(r.error).toContain("not pinned to a digest");
|
|
74
112
|
});
|
|
75
113
|
|
|
76
114
|
it("surfaces a gcloud failure", async () => {
|
|
77
115
|
const { run } = fakeGcloud({ "services list": [fail("PERMISSION_DENIED")] });
|
|
78
|
-
const r = await readDeployedImageDigests({
|
|
116
|
+
const r = await readDeployedImageDigests({ ...FIREBASE, run });
|
|
79
117
|
expect(r).toEqual({ ok: false, error: "list Cloud Run services: PERMISSION_DENIED" });
|
|
80
118
|
});
|
|
81
119
|
});
|
|
@@ -87,40 +125,50 @@ describe("checkDeployedRuntimeEnv", () => {
|
|
|
87
125
|
name,
|
|
88
126
|
valueFrom: { secretKeyRef: { name: `caelo-production-${id}`, key: "latest" } },
|
|
89
127
|
});
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
const adminOk = svc([
|
|
129
|
+
secret("ADMIN_DATABASE_PASSWORD"),
|
|
130
|
+
secret("PUBLIC_ADMIN_DATABASE_PASSWORD"),
|
|
131
|
+
]);
|
|
132
|
+
const gatewayOk = svc([secret("PUBLIC_DATABASE_PASSWORD", "public-role-password")]);
|
|
133
|
+
const proxyOk = svc([secret("CAELO_MCP_OAUTH_SEAL_KEY", "mcp-oauth-seal-key")]);
|
|
94
134
|
|
|
95
|
-
it("passes once
|
|
135
|
+
it("passes once every service runs the current env", async () => {
|
|
96
136
|
const { run } = fakeGcloud({
|
|
97
|
-
...
|
|
98
|
-
"describe caelo-production-admin-abc": [
|
|
99
|
-
|
|
100
|
-
],
|
|
101
|
-
"describe caelo-production-gateway-def": [
|
|
102
|
-
svc([secret("PUBLIC_DATABASE_PASSWORD", "public-role-password")]),
|
|
103
|
-
],
|
|
137
|
+
...firebaseLists(),
|
|
138
|
+
"describe caelo-production-admin-abc": [adminOk],
|
|
139
|
+
"describe caelo-production-data-gateway": [gatewayOk],
|
|
140
|
+
"describe caelo-production-gateway-def": [proxyOk],
|
|
104
141
|
});
|
|
105
|
-
expect(await checkDeployedRuntimeEnv({
|
|
106
|
-
|
|
142
|
+
expect(await checkDeployedRuntimeEnv({ ...FIREBASE, run })).toEqual({ ok: true });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("#1328 — stops a wizard re-run while the proxy is still the old gateway (database env)", async () => {
|
|
146
|
+
const { run } = fakeGcloud({
|
|
147
|
+
...firebaseLists(),
|
|
148
|
+
"describe caelo-production-admin-abc": [adminOk],
|
|
149
|
+
"describe caelo-production-data-gateway": [gatewayOk],
|
|
150
|
+
"describe caelo-production-gateway-def": [gatewayOk],
|
|
107
151
|
});
|
|
152
|
+
const r = await checkDeployedRuntimeEnv({ ...FIREBASE, run });
|
|
153
|
+
expect(r.ok).toBe(false);
|
|
154
|
+
if (!r.ok) {
|
|
155
|
+
expect(r.error).toContain("PUBLIC_DATABASE_PASSWORD");
|
|
156
|
+
expect(r.error).toContain("Run `upgrade` first");
|
|
157
|
+
}
|
|
108
158
|
});
|
|
109
159
|
|
|
110
160
|
it("stops a wizard re-run on a gateway from before #635 (it needs its gateway_role pool)", async () => {
|
|
111
161
|
const { run } = fakeGcloud({
|
|
112
|
-
...
|
|
113
|
-
"describe caelo-production-admin-abc": [
|
|
114
|
-
|
|
115
|
-
],
|
|
116
|
-
"describe caelo-production-gateway-def": [
|
|
162
|
+
...firebaseLists(),
|
|
163
|
+
"describe caelo-production-admin-abc": [adminOk],
|
|
164
|
+
"describe caelo-production-data-gateway": [
|
|
117
165
|
svc([
|
|
118
166
|
secret("GATEWAY_DATABASE_PASSWORD", "gateway-role-password"),
|
|
119
167
|
secret("PUBLIC_DATABASE_PASSWORD", "public-role-password"),
|
|
120
168
|
]),
|
|
121
169
|
],
|
|
122
170
|
});
|
|
123
|
-
const r = await checkDeployedRuntimeEnv({
|
|
171
|
+
const r = await checkDeployedRuntimeEnv({ ...FIREBASE, run });
|
|
124
172
|
expect(r.ok).toBe(false);
|
|
125
173
|
if (!r.ok) {
|
|
126
174
|
expect(r.error).toContain("GATEWAY_DATABASE_PASSWORD");
|
|
@@ -130,15 +178,13 @@ describe("checkDeployedRuntimeEnv", () => {
|
|
|
130
178
|
|
|
131
179
|
it("stops a wizard re-run on a gateway from before #613 (it needs the admin_role pool)", async () => {
|
|
132
180
|
const { run } = fakeGcloud({
|
|
133
|
-
...
|
|
134
|
-
"describe caelo-production-admin-abc": [
|
|
135
|
-
|
|
136
|
-
],
|
|
137
|
-
"describe caelo-production-gateway-def": [
|
|
181
|
+
...firebaseLists(),
|
|
182
|
+
"describe caelo-production-admin-abc": [adminOk],
|
|
183
|
+
"describe caelo-production-data-gateway": [
|
|
138
184
|
svc([secret("ADMIN_DATABASE_PASSWORD"), secret("PUBLIC_DATABASE_PASSWORD")]),
|
|
139
185
|
],
|
|
140
186
|
});
|
|
141
|
-
const r = await checkDeployedRuntimeEnv({
|
|
187
|
+
const r = await checkDeployedRuntimeEnv({ ...FIREBASE, run });
|
|
142
188
|
expect(r.ok).toBe(false);
|
|
143
189
|
if (!r.ok) {
|
|
144
190
|
expect(r.error).toContain("ADMIN_DATABASE_PASSWORD");
|
|
@@ -148,14 +194,17 @@ describe("checkDeployedRuntimeEnv", () => {
|
|
|
148
194
|
|
|
149
195
|
it("stops a wizard re-run on an install upgrade hasn't moved over (password in the URL)", async () => {
|
|
150
196
|
const { run } = fakeGcloud({
|
|
151
|
-
...
|
|
197
|
+
...firebaseLists(),
|
|
152
198
|
"describe caelo-production-admin-abc": [
|
|
153
199
|
svc([
|
|
154
|
-
{
|
|
200
|
+
{
|
|
201
|
+
name: "ADMIN_DATABASE_URL",
|
|
202
|
+
value: "postgres://admin_role:pw@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin",
|
|
203
|
+
},
|
|
155
204
|
]),
|
|
156
205
|
],
|
|
157
206
|
});
|
|
158
|
-
const r = await checkDeployedRuntimeEnv({
|
|
207
|
+
const r = await checkDeployedRuntimeEnv({ ...FIREBASE, run });
|
|
159
208
|
expect(r.ok).toBe(false);
|
|
160
209
|
if (!r.ok) {
|
|
161
210
|
expect(r.error).toContain("Run `upgrade` first");
|
|
@@ -165,29 +214,44 @@ describe("checkDeployedRuntimeEnv", () => {
|
|
|
165
214
|
});
|
|
166
215
|
|
|
167
216
|
describe("chooseImageDigests", () => {
|
|
168
|
-
const latest = { admin: C, gateway: C };
|
|
217
|
+
const latest = { admin: C, gateway: C, "mcp-auth-proxy": C };
|
|
218
|
+
const recorded = { admin: A, gateway: B, "mcp-auth-proxy": C };
|
|
169
219
|
const never = () => {
|
|
170
220
|
throw new Error("must not be called");
|
|
171
221
|
};
|
|
172
222
|
|
|
173
223
|
it("keeps the recorded release", async () => {
|
|
224
|
+
const r = await chooseImageDigests({
|
|
225
|
+
recorded,
|
|
226
|
+
deployed: true,
|
|
227
|
+
readLive: never,
|
|
228
|
+
resolveLatest: never,
|
|
229
|
+
});
|
|
230
|
+
expect(r).toEqual({ ok: true, source: "recorded", digests: recorded });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("#1328 — sends a record from before the MCP-Auth-Proxy to upgrade instead of mixing releases", async () => {
|
|
174
234
|
const r = await chooseImageDigests({
|
|
175
235
|
recorded: { admin: A, gateway: B },
|
|
176
236
|
deployed: true,
|
|
177
237
|
readLive: never,
|
|
178
238
|
resolveLatest: never,
|
|
179
239
|
});
|
|
180
|
-
expect(r).
|
|
240
|
+
expect(r.ok).toBe(false);
|
|
241
|
+
if (!r.ok) {
|
|
242
|
+
expect(r.error).toContain("records no mcp-auth-proxy image");
|
|
243
|
+
expect(r.error).toContain("Run `upgrade` first");
|
|
244
|
+
}
|
|
181
245
|
});
|
|
182
246
|
|
|
183
247
|
it("keeps what a provisioned install without a record runs (no :latest roll)", async () => {
|
|
184
248
|
const r = await chooseImageDigests({
|
|
185
249
|
recorded: null,
|
|
186
250
|
deployed: true,
|
|
187
|
-
readLive: async () => ({ ok: true, digests:
|
|
251
|
+
readLive: async () => ({ ok: true, digests: recorded }),
|
|
188
252
|
resolveLatest: never,
|
|
189
253
|
});
|
|
190
|
-
expect(r).toEqual({ ok: true, source: "live", digests:
|
|
254
|
+
expect(r).toEqual({ ok: true, source: "live", digests: recorded });
|
|
191
255
|
});
|
|
192
256
|
|
|
193
257
|
it("stops and points at upgrade when the running release can't be read", async () => {
|
package/src/deployed-release.ts
CHANGED
|
@@ -15,7 +15,14 @@ import { gcloud as defaultGcloud } from "./gcloud.js";
|
|
|
15
15
|
import type { GcloudRunner } from "./gcloud-retry.js";
|
|
16
16
|
import { GCP_STACK_ENV, gcpNamePrefix } from "./gcp-names.js";
|
|
17
17
|
import type { ImageDigests } from "./install-state.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
CLOUD_RUN_SLUGS,
|
|
20
|
+
type CloudRunSlug,
|
|
21
|
+
cloudRunServicePrefix,
|
|
22
|
+
type GcpProvider,
|
|
23
|
+
RETIRED_SERVICE_ENV,
|
|
24
|
+
SERVICE_SECRET_ENV,
|
|
25
|
+
} from "./stack-contract.js";
|
|
19
26
|
import { liveContainerEnv } from "./stack-converge.js";
|
|
20
27
|
|
|
21
28
|
/** The `sha256:…` digest an image reference pins, or null for a tag reference. */
|
|
@@ -23,13 +30,13 @@ export function digestFromImageRef(ref: string): string | null {
|
|
|
23
30
|
return ref.trim().match(/@(sha256:[0-9a-f]{64})$/)?.[1] ?? null;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
/** The single deployed
|
|
33
|
+
/** The single deployed Cloud Run service playing `slug` (stack-contract.ts cloudRunServicePrefix). */
|
|
27
34
|
async function findDeployedService(
|
|
28
35
|
run: GcloudRunner,
|
|
29
|
-
opts: { projectId: string; region: string },
|
|
36
|
+
opts: { provider: GcpProvider; projectId: string; region: string },
|
|
30
37
|
slug: CloudRunSlug,
|
|
31
38
|
): Promise<{ ok: true; name: string } | { ok: false; error: string }> {
|
|
32
|
-
const prefix =
|
|
39
|
+
const prefix = cloudRunServicePrefix(opts.provider, GCP_STACK_ENV, slug);
|
|
33
40
|
const list = await run([
|
|
34
41
|
"run",
|
|
35
42
|
"services",
|
|
@@ -47,7 +54,10 @@ async function findDeployedService(
|
|
|
47
54
|
if (names.length !== 1) {
|
|
48
55
|
return {
|
|
49
56
|
ok: false,
|
|
50
|
-
error:
|
|
57
|
+
error:
|
|
58
|
+
names.length === 0
|
|
59
|
+
? `no ${prefix}* Cloud Run service: the install runs a release from before it (the ${slug} became its own service in #1328). Run \`upgrade\` first: it creates the service and moves the release over.`
|
|
60
|
+
: `expected one ${prefix}* Cloud Run service, found ${names.join(", ")}`,
|
|
51
61
|
};
|
|
52
62
|
}
|
|
53
63
|
return { ok: true, name: names[0] as string };
|
|
@@ -68,12 +78,13 @@ async function findDeployedService(
|
|
|
68
78
|
* and the env in one revision.
|
|
69
79
|
*/
|
|
70
80
|
export async function checkDeployedRuntimeEnv(opts: {
|
|
81
|
+
provider: GcpProvider;
|
|
71
82
|
projectId: string;
|
|
72
83
|
region: string;
|
|
73
84
|
run?: GcloudRunner;
|
|
74
85
|
}): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
75
86
|
const run = opts.run ?? defaultGcloud;
|
|
76
|
-
for (const slug of
|
|
87
|
+
for (const slug of CLOUD_RUN_SLUGS) {
|
|
77
88
|
const found = await findDeployedService(run, opts, slug);
|
|
78
89
|
if (!found.ok) return found;
|
|
79
90
|
const describe = await run([
|
|
@@ -109,18 +120,19 @@ export async function checkDeployedRuntimeEnv(opts: {
|
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
/**
|
|
112
|
-
* Read the image digests the
|
|
123
|
+
* Read the image digests the Cloud Run services run. Fails, naming
|
|
113
124
|
* what is missing, when a service can't be found unambiguously or doesn't
|
|
114
125
|
* run a digest-pinned image.
|
|
115
126
|
*/
|
|
116
127
|
export async function readDeployedImageDigests(opts: {
|
|
128
|
+
provider: GcpProvider;
|
|
117
129
|
projectId: string;
|
|
118
130
|
region: string;
|
|
119
131
|
run?: GcloudRunner;
|
|
120
132
|
}): Promise<{ ok: true; digests: ImageDigests } | { ok: false; error: string }> {
|
|
121
133
|
const run = opts.run ?? defaultGcloud;
|
|
122
|
-
const digests = { admin: "", gateway: "" };
|
|
123
|
-
for (const slug of
|
|
134
|
+
const digests: Record<CloudRunSlug, string> = { admin: "", gateway: "", "mcp-auth-proxy": "" };
|
|
135
|
+
for (const slug of CLOUD_RUN_SLUGS) {
|
|
124
136
|
const found = await findDeployedService(run, opts, slug);
|
|
125
137
|
if (!found.ok) return found;
|
|
126
138
|
const image = await run([
|
|
@@ -170,6 +182,15 @@ export async function chooseImageDigests(opts: {
|
|
|
170
182
|
readonly readLive: () => ReturnType<typeof readDeployedImageDigests>;
|
|
171
183
|
readonly resolveLatest: () => Promise<ImageDigests>;
|
|
172
184
|
}): Promise<DigestChoice> {
|
|
185
|
+
if (opts.recorded && !opts.recorded["mcp-auth-proxy"]) {
|
|
186
|
+
// #1328 — the release it records predates the MCP-Auth-Proxy: this
|
|
187
|
+
// installer's stack would deploy a proxy image of another release.
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
error:
|
|
191
|
+
"This install runs a release from before the MCP-Auth-Proxy (its install.json records no mcp-auth-proxy image). Run `upgrade` first — it sets the proxy up and records the release — then re-run the installer.",
|
|
192
|
+
};
|
|
193
|
+
}
|
|
173
194
|
if (opts.recorded) return { ok: true, source: "recorded", digests: opts.recorded };
|
|
174
195
|
if (!opts.deployed) return { ok: true, source: "latest", digests: await opts.resolveLatest() };
|
|
175
196
|
const live = await opts.readLive();
|
|
@@ -65,7 +65,7 @@ describe("gcp + gcp-firebase: the gateway env contract (#635)", () => {
|
|
|
65
65
|
domain: "example.com",
|
|
66
66
|
region: "europe-west1",
|
|
67
67
|
stagingHost: "staging.example.com",
|
|
68
|
-
databaseUrls: databaseUrls("
|
|
68
|
+
databaseUrls: databaseUrls("db1.p-label.europe-west1.sql-psa.goog"),
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
it("carries no cms_admin URL, role or credential name", () => {
|
|
@@ -75,7 +75,8 @@ describe("gcp + gcp-firebase: the gateway env contract (#635)", () => {
|
|
|
75
75
|
}
|
|
76
76
|
expect(env.find((v) => v.name === "PUBLIC_DATABASE_URL")).toEqual({
|
|
77
77
|
name: "PUBLIC_DATABASE_URL",
|
|
78
|
-
value:
|
|
78
|
+
value:
|
|
79
|
+
"postgresql://public_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
79
80
|
});
|
|
80
81
|
});
|
|
81
82
|
|
|
@@ -135,6 +136,7 @@ describe("self-hosted: the generated compose gateway service (#635)", () => {
|
|
|
135
136
|
postgresPassword: "superpw",
|
|
136
137
|
rolePasswords: { admin: "adminpw", public: "publicpw" },
|
|
137
138
|
caeloSecretKek: "0".repeat(64),
|
|
139
|
+
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
138
140
|
internalSecret: "i".repeat(48),
|
|
139
141
|
diskSize: "20Gi",
|
|
140
142
|
images: {
|
|
@@ -151,9 +153,12 @@ describe("self-hosted: the generated compose gateway service (#635)", () => {
|
|
|
151
153
|
expect(gateway).toContain("PUBLIC_DATABASE_URL:");
|
|
152
154
|
expect(gateway).not.toMatch(ADMIN_ACCESS);
|
|
153
155
|
expect(gateway).not.toMatch(/superpw|adminpw|gatewaypw/);
|
|
154
|
-
// No other way in: no Compose secrets, no env file,
|
|
155
|
-
//
|
|
156
|
-
expect(gateway).not.toMatch(/^ {4}(secrets|env_file
|
|
156
|
+
// No other way in: no Compose secrets, no env file, and exactly one
|
|
157
|
+
// mount — the database CA certificate (#1338), public, no credential.
|
|
158
|
+
expect(gateway).not.toMatch(/^ {4}(secrets|env_file):/m);
|
|
159
|
+
expect(gateway).toContain(
|
|
160
|
+
" volumes:\n - ./postgres-tls/ca.crt:/etc/caelo/db-ca/self-hosted-ca.pem:ro\n restart:",
|
|
161
|
+
);
|
|
157
162
|
const env = gateway.slice(gateway.indexOf(" environment:"));
|
|
158
163
|
const keys = [...env.matchAll(/^ {6}([A-Z_][A-Z0-9_]*):/gm)].map((m) => m[1]);
|
|
159
164
|
expect(keys.sort()).toEqual([
|
package/src/gcp-names.ts
CHANGED
|
@@ -56,6 +56,37 @@ export function gatewayServiceAccountEmail(projectId: string, env: string): stri
|
|
|
56
56
|
return `${gatewayServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* #1328 — account id of the service account the MCP-Auth-Proxy runs as. It
|
|
61
|
+
* may read the Google sign-in client and the broker's seal key and nothing
|
|
62
|
+
* else: no database password, no project role (stack-contract.ts
|
|
63
|
+
* `SERVICE_SECRET_ENV` / `SERVICE_SECRET_FILES`). Shortened from the
|
|
64
|
+
* service's name: account ids max out at 30 characters.
|
|
65
|
+
*/
|
|
66
|
+
export function mcpAuthProxyServiceAccountId(env: string): string {
|
|
67
|
+
return `${gcpNamePrefix(env)}-mcp-proxy-sa`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* #1328 — the `gcp` MCP-Auth-Proxy's load-balancer resources, CLI-owned
|
|
72
|
+
* (remote-mcp.ts ensureRemoteMcpEdge): its serverless NEG, its backend
|
|
73
|
+
* service and its own Cloud Armor policy. None matches a prefix the other
|
|
74
|
+
* lookups use (`…-waf`, `…-admin-backend`, `…-gateway-backend`).
|
|
75
|
+
*/
|
|
76
|
+
export function mcpAuthProxyEdgeNames(env: string): {
|
|
77
|
+
readonly neg: string;
|
|
78
|
+
readonly backend: string;
|
|
79
|
+
readonly policy: string;
|
|
80
|
+
} {
|
|
81
|
+
const prefix = `${gcpNamePrefix(env)}-mcp-auth-proxy`;
|
|
82
|
+
return { neg: `${prefix}-neg`, backend: `${prefix}-backend`, policy: `${prefix}-armor` };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Email of the service account the MCP-Auth-Proxy runs as. */
|
|
86
|
+
export function mcpAuthProxyServiceAccountEmail(projectId: string, env: string): string {
|
|
87
|
+
return `${mcpAuthProxyServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
|
|
88
|
+
}
|
|
89
|
+
|
|
59
90
|
/**
|
|
60
91
|
* Account id of the `gcp` stack's static-publisher service account. Account
|
|
61
92
|
* ids max out at 30 chars, so the env is shortened.
|
package/src/gcp-url-map.test.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { beforeAll, describe, expect, it } from "bun:test";
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import { GCP_GATEWAY_PATHS, isGcpRedirectRule, withGcpRedirects } from "@caelo-cms/shared";
|
|
13
13
|
import * as pulumi from "@pulumi/pulumi";
|
|
14
14
|
import { URL_MAP_REDIRECTS_PERMISSIONS, urlMapRedirectsRoleId } from "./gcp-names.js";
|
|
15
15
|
|
|
@@ -66,7 +66,8 @@ describe("gcp stack URL map (Pulumi mocks)", () => {
|
|
|
66
66
|
{
|
|
67
67
|
name: PUBLIC_PATH_MATCHER,
|
|
68
68
|
defaultService: "bucket-id",
|
|
69
|
-
|
|
69
|
+
// #1328 — only /api/* here: the MCP-Auth-Proxy's rule is the CLI's.
|
|
70
|
+
pathRules: [{ paths: [...GCP_GATEWAY_PATHS], service: "gateway-id" }],
|
|
70
71
|
},
|
|
71
72
|
{ name: "admin", defaultService: "admin-id" },
|
|
72
73
|
]);
|
|
@@ -102,12 +103,9 @@ describe("gcp stack URL map (Pulumi mocks)", () => {
|
|
|
102
103
|
);
|
|
103
104
|
expect(withRedirects.pathRules?.filter((r) => !isGcpRedirectRule(r))).toEqual([
|
|
104
105
|
{
|
|
105
|
-
paths
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"/.well-known/oauth-protected-resource/*",
|
|
109
|
-
"/.well-known/oauth-authorization-server/*",
|
|
110
|
-
],
|
|
106
|
+
// #1328 — the remote-MCP discovery paths go to the MCP-Auth-Proxy's
|
|
107
|
+
// own rule, which the CLI adds (remote-mcp.ts planUrlMapPaths).
|
|
108
|
+
paths: ["/api/*"],
|
|
111
109
|
service: "gateway-id",
|
|
112
110
|
},
|
|
113
111
|
]);
|
package/src/gcp-url-map.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Imported by the stack from `dist/`; keep it free of CLI dependencies.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { GCP_GATEWAY_PATHS } from "@caelo-cms/shared";
|
|
23
23
|
import * as gcp from "@pulumi/gcp";
|
|
24
24
|
import type * as pulumi from "@pulumi/pulumi";
|
|
25
25
|
|
|
@@ -58,7 +58,11 @@ export function siteUrlMapArgs(i: SiteUrlMapInputs): gcp.compute.URLMapArgs {
|
|
|
58
58
|
{
|
|
59
59
|
name: PUBLIC_PATH_MATCHER,
|
|
60
60
|
defaultService: i.staticBackend,
|
|
61
|
-
|
|
61
|
+
// #1328 — the MCP-Auth-Proxy's rule (GCP_MCP_AUTH_PROXY_PATHS) is the
|
|
62
|
+
// CLI's: it creates the proxy's backend after the first `up`
|
|
63
|
+
// (remote-mcp.ts ensureRemoteMcpEdge), and the stack ignores
|
|
64
|
+
// `pathRules` after creation anyway.
|
|
65
|
+
pathRules: [{ paths: [...GCP_GATEWAY_PATHS], service: i.gatewayBackend }],
|
|
62
66
|
},
|
|
63
67
|
{
|
|
64
68
|
name: "admin",
|
package/src/index.test.ts
CHANGED
|
@@ -22,6 +22,7 @@ describe("Caddyfile generator", () => {
|
|
|
22
22
|
tls: "acme",
|
|
23
23
|
adminUpstream: "caelo-admin:5173",
|
|
24
24
|
gatewayUpstream: "caelo-gateway:8090",
|
|
25
|
+
mcpAuthProxyUpstream: "caelo-mcp-auth-proxy:8095",
|
|
25
26
|
domains: [
|
|
26
27
|
{ hostname: "example.com", kind: "public", env: "production" },
|
|
27
28
|
{ hostname: "staging.example.com", kind: "public", env: "staging" },
|
|
@@ -49,6 +50,7 @@ describe("Caddyfile generator", () => {
|
|
|
49
50
|
tls: "acme",
|
|
50
51
|
adminUpstream: "caelo-admin:5173",
|
|
51
52
|
gatewayUpstream: "caelo-gateway:8090",
|
|
53
|
+
mcpAuthProxyUpstream: "caelo-mcp-auth-proxy:8095",
|
|
52
54
|
domains: [],
|
|
53
55
|
});
|
|
54
56
|
expect(out).toContain(":8081 {");
|
|
@@ -64,6 +66,7 @@ describe("Caddy cache policy for content-hashed assets", () => {
|
|
|
64
66
|
tls: "acme",
|
|
65
67
|
adminUpstream: "caelo-admin:5173",
|
|
66
68
|
gatewayUpstream: "caelo-gateway:8090",
|
|
69
|
+
mcpAuthProxyUpstream: "caelo-mcp-auth-proxy:8095",
|
|
67
70
|
domains: [
|
|
68
71
|
{ hostname: "example.com", kind: "public", env: "production" },
|
|
69
72
|
{ hostname: "admin.example.com", kind: "admin", env: "production" },
|
|
@@ -151,6 +154,7 @@ describe("docker-compose generator", () => {
|
|
|
151
154
|
postgresPassword: "supersecret",
|
|
152
155
|
rolePasswords: { admin: "adminpw", public: "publicpw" },
|
|
153
156
|
caeloSecretKek: "0".repeat(64),
|
|
157
|
+
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
154
158
|
internalSecret: "i".repeat(48),
|
|
155
159
|
anthropicApiKey: "sk-ant-…",
|
|
156
160
|
diskSize: "20Gi",
|
|
@@ -173,6 +177,7 @@ describe("docker-compose generator", () => {
|
|
|
173
177
|
postgresPassword: "supersecret",
|
|
174
178
|
rolePasswords: { admin: "adminpw", public: "publicpw" },
|
|
175
179
|
caeloSecretKek: "0".repeat(64),
|
|
180
|
+
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
176
181
|
internalSecret: "i".repeat(48),
|
|
177
182
|
diskSize: "20Gi",
|
|
178
183
|
images: RELEASE_IMAGES,
|
|
@@ -181,7 +186,7 @@ describe("docker-compose generator", () => {
|
|
|
181
186
|
});
|
|
182
187
|
const gateway = out.slice(out.indexOf("caelo-gateway:"), out.indexOf("\nvolumes:"));
|
|
183
188
|
expect(gateway).toContain(
|
|
184
|
-
'PUBLIC_DATABASE_URL: "postgres://public_role:publicpw@postgres:5432/cms_public"',
|
|
189
|
+
'PUBLIC_DATABASE_URL: "postgres://public_role:publicpw@postgres:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/self-hosted-ca.pem"',
|
|
185
190
|
);
|
|
186
191
|
expect(gateway).not.toMatch(
|
|
187
192
|
/ADMIN_DATABASE|GATEWAY_DATABASE|cms_admin|admin_role|gateway_role|adminpw|gatewaypw|supersecret|CAELO_SECRET_KEK/,
|
|
@@ -198,6 +203,7 @@ describe("docker-compose generator", () => {
|
|
|
198
203
|
postgresPassword: 'pa"ss',
|
|
199
204
|
rolePasswords: { admin: "a", public: "b" },
|
|
200
205
|
caeloSecretKek: "0".repeat(64),
|
|
206
|
+
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
201
207
|
internalSecret: "i".repeat(48),
|
|
202
208
|
diskSize: "1Gi",
|
|
203
209
|
images: RELEASE_IMAGES,
|
package/src/index.ts
CHANGED
|
@@ -20,8 +20,8 @@ describe("infrastructure contract steps", () => {
|
|
|
20
20
|
);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
it("is at
|
|
24
|
-
expect(INFRA_CONTRACT_VERSION).toBe(
|
|
23
|
+
it("is at 16: public_role revoke (3), URL-map redirects role (4), no gateway cms_admin login (5), ops-bucket list (6), gateway proxy hops (7), quality-audit job (8), converge job (9), remote MCP (10), Google sign-in (11), project-level sign-in client (12), edge security headers (13), capture job (14), database verify-full TLS (15), MCP-Auth-Proxy (16)", () => {
|
|
24
|
+
expect(INFRA_CONTRACT_VERSION).toBe(16);
|
|
25
25
|
expect(INFRA_CONTRACT_STEPS[3]).toContain("#633");
|
|
26
26
|
expect(INFRA_CONTRACT_STEPS[4]).toContain("#587");
|
|
27
27
|
expect(INFRA_CONTRACT_STEPS[5]).toContain("#635");
|
|
@@ -34,6 +34,8 @@ describe("infrastructure contract steps", () => {
|
|
|
34
34
|
expect(INFRA_CONTRACT_STEPS[12]).toContain("project's IAP settings");
|
|
35
35
|
expect(INFRA_CONTRACT_STEPS[13]).toContain("#891");
|
|
36
36
|
expect(INFRA_CONTRACT_STEPS[14]).toContain("#1217");
|
|
37
|
+
expect(INFRA_CONTRACT_STEPS[15]).toContain("#1338");
|
|
38
|
+
expect(INFRA_CONTRACT_STEPS[16]).toContain("#1328");
|
|
37
39
|
});
|
|
38
40
|
|
|
39
41
|
it("lists the pending steps oldest first", () => {
|
|
@@ -44,6 +46,6 @@ describe("infrastructure contract steps", () => {
|
|
|
44
46
|
});
|
|
45
47
|
|
|
46
48
|
it("names a version a newer release added by number", () => {
|
|
47
|
-
expect(infraStepsText(
|
|
49
|
+
expect(infraStepsText(16, 17)).toBe("infrastructure step 17 (see that release's notes)");
|
|
48
50
|
});
|
|
49
51
|
});
|