@caelo-cms/provisioning 0.11.8 → 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 +65 -3
- 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
|
@@ -17,11 +17,15 @@ import { IMAGE_DIGEST } from "@caelo-cms/shared";
|
|
|
17
17
|
import { confirm, isCancel, log, spinner } from "@clack/prompts";
|
|
18
18
|
import { green, red } from "kleur/colors";
|
|
19
19
|
|
|
20
|
-
/** The
|
|
21
|
-
export type ReleaseService = "admin" | "gateway";
|
|
20
|
+
/** The Caelo services shipped as release images (#1328 added the MCP-Auth-Proxy). */
|
|
21
|
+
export type ReleaseService = "admin" | "gateway" | "mcp-auth-proxy";
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The services every install runs, in roll order: the admin first, then the
|
|
25
|
+
* gateway, then the MCP-Auth-Proxy (its own service since #1328, so a hole in
|
|
26
|
+
* a visitor handler never shares a process with the operators' tokens).
|
|
27
|
+
*/
|
|
28
|
+
export const RELEASE_SERVICES: readonly ReleaseService[] = ["admin", "gateway", "mcp-auth-proxy"];
|
|
25
29
|
|
|
26
30
|
/** GHCR — the canonical registry (release-images.yml), used by self-hosted Compose. */
|
|
27
31
|
export const GHCR_REGISTRY = "ghcr.io";
|
|
@@ -116,7 +120,13 @@ export function releaseTag(opts: {
|
|
|
116
120
|
return "latest";
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
|
|
123
|
+
/**
|
|
124
|
+
* `notFound`: the registry answered that the tag does not exist (HTTP 404)
|
|
125
|
+
* — a verdict, unlike an outage, a refused token or a malformed answer.
|
|
126
|
+
*/
|
|
127
|
+
export type DigestResult =
|
|
128
|
+
| { ok: true; digest: string }
|
|
129
|
+
| { ok: false; reason: string; notFound?: true };
|
|
120
130
|
|
|
121
131
|
/** Manifest media types: single-arch manifests and multi-arch indexes, Docker + OCI. */
|
|
122
132
|
const MANIFEST_ACCEPT = [
|
|
@@ -155,7 +165,13 @@ export async function resolveRegistryDigest(
|
|
|
155
165
|
headers: { Accept: MANIFEST_ACCEPT, Authorization: `Bearer ${token.token}` },
|
|
156
166
|
});
|
|
157
167
|
}
|
|
158
|
-
if (!res.ok)
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
reason: `HTTP ${res.status} ${res.statusText}`,
|
|
172
|
+
...(res.status === 404 ? { notFound: true as const } : {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
159
175
|
const header = res.headers.get("docker-content-digest");
|
|
160
176
|
const digest = header ? canonicalDigest(header) : null;
|
|
161
177
|
if (!digest) {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { describe, expect, it } from "bun:test";
|
|
11
11
|
import type { GcloudResult } from "../gcloud.js";
|
|
12
12
|
import type { GcloudRunner } from "../gcloud-retry.js";
|
|
13
|
+
import { MCP_ARMOR_RULE } from "../remote-mcp-edge.js";
|
|
13
14
|
import type { HttpFetch } from "../runtime-secrets.js";
|
|
14
15
|
import {
|
|
15
16
|
type AppRollInstall,
|
|
@@ -34,13 +35,26 @@ const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitC
|
|
|
34
35
|
const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
|
|
35
36
|
|
|
36
37
|
const ADMIN = "caelo-production-admin-aaa";
|
|
37
|
-
|
|
38
|
+
/** gcp-firebase: the visitor gateway, under its fixed name since #1328. */
|
|
39
|
+
const GATEWAY = "caelo-production-data-gateway";
|
|
40
|
+
/** gcp-firebase: the service that was the gateway and is the MCP-Auth-Proxy (E13 a). */
|
|
41
|
+
const PROXY = "caelo-production-gateway-bbb";
|
|
42
|
+
/** gcp: the proxy's fixed name (the gateway keeps its auto-named service). */
|
|
43
|
+
const GCP_PROXY = "caelo-production-mcp-auth-proxy";
|
|
38
44
|
const SQL = "caelo-production-pg-abc";
|
|
39
45
|
const BACKUP_ID = "1760000000000";
|
|
40
|
-
const DIGEST = {
|
|
46
|
+
const DIGEST = {
|
|
47
|
+
admin: `sha256:${"a".repeat(64)}`,
|
|
48
|
+
gateway: `sha256:${"b".repeat(64)}`,
|
|
49
|
+
"mcp-auth-proxy": `sha256:${"d".repeat(64)}`,
|
|
50
|
+
};
|
|
41
51
|
const IMAGES: Record<RollSlug, ReleaseImage> = {
|
|
42
52
|
admin: { digest: DIGEST.admin, imageRef: `registry/admin@${DIGEST.admin}` },
|
|
43
53
|
gateway: { digest: DIGEST.gateway, imageRef: `registry/gateway@${DIGEST.gateway}` },
|
|
54
|
+
"mcp-auth-proxy": {
|
|
55
|
+
digest: DIGEST["mcp-auth-proxy"],
|
|
56
|
+
imageRef: `registry/mcp-auth-proxy@${DIGEST["mcp-auth-proxy"]}`,
|
|
57
|
+
},
|
|
44
58
|
};
|
|
45
59
|
|
|
46
60
|
const secretRef = (name: string, secret: string) => ({
|
|
@@ -53,7 +67,7 @@ const secretRef = (name: string, secret: string) => ({
|
|
|
53
67
|
* returns it. `ingress`: the service-level setting (#732). Always-on CPU,
|
|
54
68
|
* as every install the stacks created before `cpuIdle` was set runs.
|
|
55
69
|
*/
|
|
56
|
-
const serviceJson = (image: string, env: unknown[], ingress = "all") =>
|
|
70
|
+
const serviceJson = (image: string, env: unknown[], ingress = "all", vpc = true) =>
|
|
57
71
|
JSON.stringify({
|
|
58
72
|
metadata: { annotations: { "run.googleapis.com/ingress": ingress } },
|
|
59
73
|
spec: {
|
|
@@ -61,9 +75,13 @@ const serviceJson = (image: string, env: unknown[], ingress = "all") =>
|
|
|
61
75
|
metadata: {
|
|
62
76
|
annotations: {
|
|
63
77
|
"run.googleapis.com/cpu-throttling": "false",
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
|
|
78
|
+
...(vpc
|
|
79
|
+
? {
|
|
80
|
+
"run.googleapis.com/network-interfaces": JSON.stringify([
|
|
81
|
+
{ network: "caelo-production-vpc", subnetwork: "caelo-production-subnet" },
|
|
82
|
+
]),
|
|
83
|
+
}
|
|
84
|
+
: {}),
|
|
67
85
|
},
|
|
68
86
|
},
|
|
69
87
|
spec: { containers: [{ image, env }] },
|
|
@@ -77,11 +95,13 @@ const ADMIN_JSON = serviceJson(OLD_ADMIN_IMAGE, [
|
|
|
77
95
|
{ name: "CAELO_FIREBASE_SITE", value: "caelo-production-site-abc123" },
|
|
78
96
|
{
|
|
79
97
|
name: "ADMIN_DATABASE_URL",
|
|
80
|
-
value:
|
|
98
|
+
value:
|
|
99
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
81
100
|
},
|
|
82
101
|
{
|
|
83
102
|
name: "PUBLIC_ADMIN_DATABASE_URL",
|
|
84
|
-
value:
|
|
103
|
+
value:
|
|
104
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
85
105
|
},
|
|
86
106
|
secretRef("ADMIN_DATABASE_PASSWORD", "postgres-password"),
|
|
87
107
|
secretRef("PUBLIC_ADMIN_DATABASE_PASSWORD", "postgres-password"),
|
|
@@ -89,10 +109,30 @@ const ADMIN_JSON = serviceJson(OLD_ADMIN_IMAGE, [
|
|
|
89
109
|
const GATEWAY_JSON = serviceJson("registry/gateway@sha256:old", [
|
|
90
110
|
{
|
|
91
111
|
name: "PUBLIC_DATABASE_URL",
|
|
92
|
-
value:
|
|
112
|
+
value:
|
|
113
|
+
"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",
|
|
114
|
+
},
|
|
115
|
+
secretRef("PUBLIC_DATABASE_PASSWORD", "public-role-password"),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* gcp-firebase: the service that was the gateway, before this upgrade turns
|
|
120
|
+
* it into the MCP-Auth-Proxy — the visitor gateway's database login, the
|
|
121
|
+
* staging gate key and the remote-MCP broker's env, on the VPC.
|
|
122
|
+
*/
|
|
123
|
+
const PROXY_JSON = serviceJson("registry/gateway@sha256:old", [
|
|
124
|
+
{
|
|
125
|
+
name: "PUBLIC_DATABASE_URL",
|
|
126
|
+
value:
|
|
127
|
+
"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",
|
|
93
128
|
},
|
|
94
129
|
secretRef("PUBLIC_DATABASE_PASSWORD", "public-role-password"),
|
|
130
|
+
secretRef("CAELO_STAGING_GATE_SECRET", "staging-gate-secret"),
|
|
131
|
+
secretRef("CAELO_MCP_OAUTH_SEAL_KEY", "mcp-oauth-seal-key"),
|
|
132
|
+
{ name: "CAELO_OPERATOR_OAUTH_CLIENT_FILE", value: "/secrets/operator-oauth-client/client.json" },
|
|
95
133
|
]);
|
|
134
|
+
/** gcp: the MCP-Auth-Proxy the converge deployed (no VPC, no database). */
|
|
135
|
+
const GCP_PROXY_JSON = serviceJson("registry/mcp-auth-proxy@sha256:old", [], "all", false);
|
|
96
136
|
|
|
97
137
|
type Answer = GcloudResult | ((line: string) => GcloudResult);
|
|
98
138
|
|
|
@@ -106,10 +146,15 @@ function fakeInstall(events: string[], overrides: [RegExp, Answer][] = []) {
|
|
|
106
146
|
const rules: [RegExp, Answer][] = [
|
|
107
147
|
...overrides,
|
|
108
148
|
[/^run services list .*caelo-production-admin/, ok(`${ADMIN}\n`)],
|
|
109
|
-
[/^run services list .*caelo-production-gateway/, ok(`${GATEWAY}\n`)],
|
|
149
|
+
[/^run services list .*caelo-production-data-gateway/, ok(`${GATEWAY}\n`)],
|
|
150
|
+
// On gcp this prefix is the gateway's, on gcp-firebase the proxy's.
|
|
151
|
+
[/^run services list .*caelo-production-gateway/, ok(`${PROXY}\n`)],
|
|
152
|
+
[/^run services list .*caelo-production-mcp-auth-proxy/, ok(`${GCP_PROXY}\n`)],
|
|
110
153
|
[/describe (\S+) .*revisionName/, (l) => ok(`${l.split(" ")[3]}-00001-old\n`)],
|
|
111
154
|
[new RegExp(`describe ${ADMIN} .*--format=json`), ok(ADMIN_JSON)],
|
|
112
155
|
[new RegExp(`describe ${GATEWAY} .*--format=json`), ok(GATEWAY_JSON)],
|
|
156
|
+
[new RegExp(`describe ${PROXY} .*--format=json`), ok(PROXY_JSON)],
|
|
157
|
+
[new RegExp(`describe ${GCP_PROXY} .*--format=json`), ok(GCP_PROXY_JSON)],
|
|
113
158
|
[/describe (\S+) .*status\.url/, (l) => ok(`https://${l.split(" ")[3]}-xyz.a.run.app\n`)],
|
|
114
159
|
[
|
|
115
160
|
/^run jobs describe caelo-production-(audit|capture) .*containers\[0\]\.image/,
|
|
@@ -247,6 +292,14 @@ const APP_ROLL_WRITE =
|
|
|
247
292
|
const ANY_WRITE =
|
|
248
293
|
/\b(create|update|update-traffic|delete|add-iam-policy-binding|remove-iam-policy-binding|set-iam-policy|execute|enable|add|restore)\b/;
|
|
249
294
|
|
|
295
|
+
/**
|
|
296
|
+
* #1328 — the only writes allowed before the backup: the MCP-Auth-Proxy's
|
|
297
|
+
* account, its secretAccessor on its two secrets, and the services an older
|
|
298
|
+
* install lacks (mcp-auth-proxy-setup.ts). Additive; nothing serves on them.
|
|
299
|
+
*/
|
|
300
|
+
const PRE_PLAN_WRITE =
|
|
301
|
+
/^(iam service-accounts create caelo-production-mcp-proxy-sa |secrets add-iam-policy-binding caelo-production-(mcp-oauth-seal-key|operator-oauth-client) .*--member=serviceAccount:caelo-production-mcp-proxy-sa@|run services replace |run deploy caelo-production-mcp-auth-proxy )/;
|
|
302
|
+
|
|
250
303
|
const indexOf = (events: string[], pattern: RegExp): number => {
|
|
251
304
|
const i = events.findIndex((e) => pattern.test(e));
|
|
252
305
|
if (i < 0) throw new Error(`no event matches ${pattern}:\n${events.join("\n")}`);
|
|
@@ -260,25 +313,31 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
260
313
|
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
261
314
|
|
|
262
315
|
const backup = indexOf(events, /^gcloud sql backups create/);
|
|
263
|
-
const firstConvergeWrite =
|
|
316
|
+
const firstConvergeWrite =
|
|
317
|
+
backup + indexOf(events.slice(backup), /^gcloud .*(add-iam-policy-binding|secrets create)/);
|
|
264
318
|
const baseline = indexOf(events, /^health baseline/);
|
|
265
319
|
const migrate = indexOf(events, /^migrate registry\/admin@/);
|
|
266
320
|
const adminRoll = indexOf(events, new RegExp(`^gcloud run services update ${ADMIN} `));
|
|
267
321
|
const gatewayRoll = indexOf(events, new RegExp(`^gcloud run services update ${GATEWAY} `));
|
|
322
|
+
const proxyRoll = indexOf(events, new RegExp(`^gcloud run services update ${PROXY} `));
|
|
268
323
|
const deep = indexOf(events, /^health deep 0\.11\.0/);
|
|
269
324
|
const digests = indexOf(events, /^record digests/);
|
|
270
325
|
|
|
271
|
-
//
|
|
326
|
+
// Before the backup only the additive pre-plan step writes (#1328): the
|
|
327
|
+
// MCP-Auth-Proxy's account and its two secret grants.
|
|
272
328
|
for (const e of events.slice(0, backup)) expect(e).not.toMatch(/^(migrate|record)/);
|
|
273
329
|
for (const e of events.slice(0, backup).filter((e) => e.startsWith("gcloud "))) {
|
|
274
|
-
|
|
330
|
+
const call = e.slice("gcloud ".length);
|
|
331
|
+
if (ANY_WRITE.test(call)) expect(call).toMatch(PRE_PLAN_WRITE);
|
|
275
332
|
}
|
|
276
333
|
expect(backup).toBeLessThan(firstConvergeWrite);
|
|
277
334
|
expect(firstConvergeWrite).toBeLessThan(baseline);
|
|
278
335
|
expect(baseline).toBeLessThan(migrate);
|
|
279
336
|
expect(migrate).toBeLessThan(adminRoll);
|
|
280
337
|
expect(adminRoll).toBeLessThan(gatewayRoll);
|
|
281
|
-
|
|
338
|
+
// The proxy rolls last: on gcp-firebase it forwards to the new gateway.
|
|
339
|
+
expect(gatewayRoll).toBeLessThan(proxyRoll);
|
|
340
|
+
expect(proxyRoll).toBeLessThan(deep);
|
|
282
341
|
expect(deep).toBeLessThan(digests);
|
|
283
342
|
expect(recorded.digests).toEqual([DIGEST]);
|
|
284
343
|
expect(lines).toContain(`done: Pre-upgrade backup ${BACKUP_ID} of ${SQL} taken`);
|
|
@@ -462,7 +521,8 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
462
521
|
});
|
|
463
522
|
expect(await upgradeGcpInstall(TARGET, deps)).toBe(false);
|
|
464
523
|
|
|
465
|
-
|
|
524
|
+
// Only the additive pre-plan grants (#1328) came before it.
|
|
525
|
+
const writes = calls.filter((c) => ANY_WRITE.test(c) && !PRE_PLAN_WRITE.test(c));
|
|
466
526
|
expect(writes).toEqual([
|
|
467
527
|
expect.stringMatching(/^sql backups create --instance caelo-production-pg-abc/),
|
|
468
528
|
]);
|
|
@@ -485,7 +545,12 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
485
545
|
const rollbacks = events
|
|
486
546
|
.slice(deep)
|
|
487
547
|
.filter((e) => e.includes("update-traffic") && e.includes("--to-revisions"));
|
|
548
|
+
// Newest first: the proxy (back to the gateway release it was), the
|
|
549
|
+
// gateway, the admin.
|
|
488
550
|
expect(rollbacks).toEqual([
|
|
551
|
+
expect.stringContaining(
|
|
552
|
+
`${PROXY} --region europe-west1 --project acme --to-revisions ${PROXY}-00001-old=100`,
|
|
553
|
+
),
|
|
489
554
|
expect.stringContaining(
|
|
490
555
|
`${GATEWAY} --region europe-west1 --project acme --to-revisions ${GATEWAY}-00001-old=100`,
|
|
491
556
|
),
|
|
@@ -624,7 +689,11 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
624
689
|
const { deps, lines } = upgradeDeps(events, {
|
|
625
690
|
deep: { ok: false, error: "unhealthy" },
|
|
626
691
|
});
|
|
627
|
-
const before = {
|
|
692
|
+
const before = {
|
|
693
|
+
admin: `${ADMIN}-00000-before`,
|
|
694
|
+
gateway: `${GATEWAY}-00000-before`,
|
|
695
|
+
"mcp-auth-proxy": `${PROXY}-00000-before`,
|
|
696
|
+
};
|
|
628
697
|
expect(
|
|
629
698
|
await upgradeGcpInstall(TARGET, {
|
|
630
699
|
...deps,
|
|
@@ -642,6 +711,27 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
642
711
|
);
|
|
643
712
|
const rollbacks = events.filter((e) => e.includes("--to-revisions"));
|
|
644
713
|
expect(rollbacks).toEqual([
|
|
714
|
+
expect.stringContaining(`--to-revisions ${before["mcp-auth-proxy"]}=100`),
|
|
715
|
+
expect.stringContaining(`--to-revisions ${before.gateway}=100`),
|
|
716
|
+
expect.stringContaining(`--to-revisions ${before.admin}=100`),
|
|
717
|
+
]);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it("#1328 — a checkpoint without the proxy's revision (an older release wrote it) keeps the planned one", async () => {
|
|
721
|
+
const events: string[] = [];
|
|
722
|
+
const { deps } = upgradeDeps(events, { deep: { ok: false, error: "unhealthy" } });
|
|
723
|
+
const before = { admin: `${ADMIN}-00000-before`, gateway: `${GATEWAY}-00000-before` };
|
|
724
|
+
expect(
|
|
725
|
+
await upgradeGcpInstall(TARGET, {
|
|
726
|
+
...deps,
|
|
727
|
+
resume: {
|
|
728
|
+
backup: { projectId: "acme", instance: SQL, backupId: "1759999999999" },
|
|
729
|
+
priorRevisions: before,
|
|
730
|
+
},
|
|
731
|
+
}),
|
|
732
|
+
).toBe(false);
|
|
733
|
+
expect(events.filter((e) => e.includes("--to-revisions"))).toEqual([
|
|
734
|
+
expect.stringContaining(`--to-revisions ${PROXY}-00001-old=100`),
|
|
645
735
|
expect.stringContaining(`--to-revisions ${before.gateway}=100`),
|
|
646
736
|
expect.stringContaining(`--to-revisions ${before.admin}=100`),
|
|
647
737
|
]);
|
|
@@ -714,7 +804,7 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
714
804
|
expect(recorded.contract).toEqual([]);
|
|
715
805
|
});
|
|
716
806
|
|
|
717
|
-
it("#806 — the rolls carry remote MCP: identity env, the
|
|
807
|
+
it("#806/#1328 — the rolls carry remote MCP: identity env, the proxy's client file and timeout", async () => {
|
|
718
808
|
const events: string[] = [];
|
|
719
809
|
const { deps, calls } = upgradeDeps(events);
|
|
720
810
|
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
@@ -723,19 +813,129 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
723
813
|
);
|
|
724
814
|
const admin = rolls.find((c) => c.startsWith(`run services update ${ADMIN} `)) ?? "";
|
|
725
815
|
const gateway = rolls.find((c) => c.startsWith(`run services update ${GATEWAY} `)) ?? "";
|
|
816
|
+
const proxy = rolls.find((c) => c.startsWith(`run services update ${PROXY} `)) ?? "";
|
|
726
817
|
expect(admin).toContain(
|
|
727
818
|
`CAELO_IAP_AUDIENCE=/projects/42/locations/europe-west1/services/${ADMIN}`,
|
|
728
819
|
);
|
|
729
|
-
|
|
730
|
-
expect(
|
|
731
|
-
|
|
732
|
-
expect(
|
|
733
|
-
|
|
820
|
+
// E13 a: the MCP address stays the run.app URL of the service that was the gateway.
|
|
821
|
+
expect(admin).toContain(`CAELO_MCP_PUBLIC_URL=https://${PROXY}-xyz.a.run.app`);
|
|
822
|
+
// The Firebase rewrite follows the data gateway.
|
|
823
|
+
expect(admin).toContain(`CAELO_GATEWAY_SERVICE=${GATEWAY}`);
|
|
824
|
+
expect(proxy).toContain(`--image ${IMAGES["mcp-auth-proxy"].imageRef}`);
|
|
825
|
+
expect(proxy).toContain(`CAELO_ADMIN_URL=https://${ADMIN}-xyz.a.run.app`);
|
|
826
|
+
expect(proxy).toContain(`CAELO_MCP_PUBLIC_URL=https://${PROXY}-xyz.a.run.app`);
|
|
827
|
+
expect(proxy).toContain(
|
|
828
|
+
"/secrets/operator-oauth-client/client.json=caelo-production-operator-oauth-client:latest",
|
|
734
829
|
);
|
|
735
|
-
expect(
|
|
830
|
+
expect(proxy).toContain("--timeout=3600");
|
|
831
|
+
// The gateway carries none of it any more.
|
|
832
|
+
expect(gateway).not.toContain("CAELO_MCP_PUBLIC_URL=");
|
|
833
|
+
expect(gateway).not.toContain("operator-oauth-client");
|
|
834
|
+
expect(gateway).not.toContain("--timeout");
|
|
736
835
|
expect(admin).not.toContain("--timeout");
|
|
737
836
|
// No extra revision: everything rides the image rolls.
|
|
738
|
-
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(
|
|
837
|
+
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(3);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it("#1328 — the gcp-firebase proxy roll leaves the VPC, drops every database login and forwards to the data gateway", async () => {
|
|
841
|
+
const events: string[] = [];
|
|
842
|
+
const { deps, calls } = upgradeDeps(events);
|
|
843
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
844
|
+
const proxy =
|
|
845
|
+
calls.find((c) => new RegExp(`^run services update ${PROXY} .*--image`).test(c)) ?? "";
|
|
846
|
+
expect(proxy).toContain(
|
|
847
|
+
"--service-account=caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com",
|
|
848
|
+
);
|
|
849
|
+
expect(proxy).toContain("--clear-network");
|
|
850
|
+
expect(proxy).toContain("CAELO_REMOTE_MCP=available");
|
|
851
|
+
expect(proxy).toContain(`CAELO_INTERIM_FORWARD_ORIGIN=https://${GATEWAY}-xyz.a.run.app`);
|
|
852
|
+
const removedEnv = proxy.match(/--remove-env-vars=(\S+)/)?.[1]?.split(",") ?? [];
|
|
853
|
+
const removedSecrets = proxy.match(/--remove-secrets=(\S+)/)?.[1]?.split(",") ?? [];
|
|
854
|
+
expect(removedEnv).toContain("PUBLIC_DATABASE_URL");
|
|
855
|
+
expect(removedSecrets).toEqual(
|
|
856
|
+
expect.arrayContaining(["PUBLIC_DATABASE_PASSWORD", "CAELO_STAGING_GATE_SECRET"]),
|
|
857
|
+
);
|
|
858
|
+
// The gateway keeps its VPC route to Cloud SQL.
|
|
859
|
+
const gateway =
|
|
860
|
+
calls.find((c) => new RegExp(`^run services update ${GATEWAY} .*--image`).test(c)) ?? "";
|
|
861
|
+
expect(gateway).not.toContain("--clear-network");
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it("#1328 — gcp-firebase: the pre-plan step copies the data gateway from the old gateway when it is missing", async () => {
|
|
865
|
+
const events: string[] = [];
|
|
866
|
+
const { deps, calls, lines } = upgradeDeps(events, {
|
|
867
|
+
overrides: [
|
|
868
|
+
[
|
|
869
|
+
new RegExp(`^run services describe ${GATEWAY} .*metadata\\.name`),
|
|
870
|
+
fail(`ERROR: Cannot find service [${GATEWAY}]. NOT_FOUND`),
|
|
871
|
+
],
|
|
872
|
+
],
|
|
873
|
+
});
|
|
874
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
875
|
+
const replace = indexOf(events, /^gcloud run services replace /);
|
|
876
|
+
expect(replace).toBeLessThan(indexOf(events, /^gcloud sql backups create/));
|
|
877
|
+
expect(lines).toContain(
|
|
878
|
+
`info: created: Cloud Run service ${GATEWAY} (the visitor gateway) (copied from ${PROXY})`,
|
|
879
|
+
);
|
|
880
|
+
// Nothing else is deployed before the plan: the proxy is rolled later.
|
|
881
|
+
expect(calls.some((c) => /^run deploy /.test(c))).toBe(false);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it("#1328 — gcp-firebase: an existing data gateway makes the pre-plan step a no-op", async () => {
|
|
885
|
+
const events: string[] = [];
|
|
886
|
+
const { deps, calls } = upgradeDeps(events);
|
|
887
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
888
|
+
expect(calls.some((c) => /^run services replace |^run deploy /.test(c))).toBe(false);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it("#1328 — a failed pre-plan step stops before the backup, with nothing rolled", async () => {
|
|
892
|
+
const events: string[] = [];
|
|
893
|
+
const { deps, calls, lines } = upgradeDeps(events, {
|
|
894
|
+
overrides: [[/^secrets add-iam-policy-binding/, fail("PERMISSION_DENIED: setIamPolicy")]],
|
|
895
|
+
});
|
|
896
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(false);
|
|
897
|
+
expect(calls.some((c) => /^sql backups create/.test(c))).toBe(false);
|
|
898
|
+
expect(calls.filter((c) => APP_ROLL_WRITE.test(c))).toEqual([]);
|
|
899
|
+
expect(events.some((e) => e.startsWith("migrate"))).toBe(false);
|
|
900
|
+
expect(
|
|
901
|
+
lines.some((l) => l.startsWith("failed: The services this release runs could not be set up")),
|
|
902
|
+
).toBe(true);
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it("#1328 — gcp: the proxy is deployed only after its image's signature verifies", async () => {
|
|
906
|
+
const missingProxy: [RegExp, Answer] = [
|
|
907
|
+
new RegExp(`^run services describe ${GCP_PROXY} .*metadata\\.name`),
|
|
908
|
+
fail(`ERROR: Cannot find service [${GCP_PROXY}]. NOT_FOUND`),
|
|
909
|
+
];
|
|
910
|
+
const events: string[] = [];
|
|
911
|
+
const { deps } = upgradeDeps(events, { overrides: [missingProxy] });
|
|
912
|
+
expect(
|
|
913
|
+
await upgradeGcpInstall(GCP_TARGET, {
|
|
914
|
+
...deps,
|
|
915
|
+
verifyImages: async (images) => {
|
|
916
|
+
events.push(`verify ${images.map((i) => i.slug).join(",")}`);
|
|
917
|
+
return true;
|
|
918
|
+
},
|
|
919
|
+
}),
|
|
920
|
+
).toBe(true);
|
|
921
|
+
const verify = indexOf(events, /^verify mcp-auth-proxy$/);
|
|
922
|
+
const deploy = indexOf(events, new RegExp(`^gcloud run deploy ${GCP_PROXY} `));
|
|
923
|
+
expect(verify).toBeLessThan(deploy);
|
|
924
|
+
expect(events[deploy]).toContain(`--image=${IMAGES["mcp-auth-proxy"].imageRef}`);
|
|
925
|
+
expect(events[deploy]).toContain("--ingress=internal-and-cloud-load-balancing");
|
|
926
|
+
expect(deploy).toBeLessThan(indexOf(events, /^gcloud sql backups create/));
|
|
927
|
+
|
|
928
|
+
// A signature that does not verify: nothing is deployed, nothing backed up.
|
|
929
|
+
const refused: string[] = [];
|
|
930
|
+
const bad = upgradeDeps(refused, { overrides: [missingProxy] });
|
|
931
|
+
expect(
|
|
932
|
+
await upgradeGcpInstall(GCP_TARGET, {
|
|
933
|
+
...bad.deps,
|
|
934
|
+
verifyImages: async (images) => !images.some((i) => i.slug === "mcp-auth-proxy"),
|
|
935
|
+
}),
|
|
936
|
+
).toBe(false);
|
|
937
|
+
expect(bad.calls.some((c) => /^run deploy /.test(c))).toBe(false);
|
|
938
|
+
expect(bad.calls.some((c) => /^sql backups create/.test(c))).toBe(false);
|
|
739
939
|
});
|
|
740
940
|
|
|
741
941
|
it("moves the gateway to request-based CPU in its image roll; the admin keeps its CPU", async () => {
|
|
@@ -747,44 +947,120 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
747
947
|
);
|
|
748
948
|
const admin = rolls.find((c) => c.startsWith(`run services update ${ADMIN} `)) ?? "";
|
|
749
949
|
const gateway = rolls.find((c) => c.startsWith(`run services update ${GATEWAY} `)) ?? "";
|
|
950
|
+
const proxy = rolls.find((c) => c.startsWith(`run services update ${PROXY} `)) ?? "";
|
|
750
951
|
expect(gateway).toContain("--cpu-throttling");
|
|
952
|
+
expect(proxy).toContain("--cpu-throttling");
|
|
751
953
|
expect(admin).not.toContain("cpu-throttling");
|
|
752
|
-
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(
|
|
954
|
+
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(3);
|
|
753
955
|
});
|
|
754
956
|
|
|
755
|
-
it("#806 — on gcp,
|
|
957
|
+
it("#806/#1328 — on gcp, moves the remote-MCP paths and the Cloud Armor rule to the proxy's backend before the roll", async () => {
|
|
756
958
|
const events: string[] = [];
|
|
959
|
+
// The #806 shape: the discovery paths on the gateway's rule, the MCP
|
|
960
|
+
// rule on the gateway's policy.
|
|
961
|
+
const gatewayBackend =
|
|
962
|
+
"https://www.googleapis.com/compute/v1/projects/acme/global/backendServices/caelo-production-gateway-backend-1";
|
|
757
963
|
const urlMap = {
|
|
758
964
|
name: "caelo-production-url-map-1",
|
|
759
965
|
hostRules: [{ hosts: ["acme.com"], pathMatcher: "public" }],
|
|
760
|
-
pathMatchers: [
|
|
966
|
+
pathMatchers: [
|
|
967
|
+
{
|
|
968
|
+
name: "public",
|
|
969
|
+
pathRules: [
|
|
970
|
+
{
|
|
971
|
+
paths: [
|
|
972
|
+
"/api/*",
|
|
973
|
+
"/.well-known/oauth-protected-resource/*",
|
|
974
|
+
"/.well-known/oauth-authorization-server/*",
|
|
975
|
+
],
|
|
976
|
+
service: gatewayBackend,
|
|
977
|
+
},
|
|
978
|
+
],
|
|
979
|
+
},
|
|
980
|
+
],
|
|
761
981
|
};
|
|
982
|
+
const r = MCP_ARMOR_RULE;
|
|
983
|
+
const gatewayMcpRule = {
|
|
984
|
+
priority: r.priority,
|
|
985
|
+
action: r.action,
|
|
986
|
+
match: { expr: { expression: r.expression } },
|
|
987
|
+
rateLimitOptions: {
|
|
988
|
+
rateLimitThreshold: { count: r.count, intervalSec: r.intervalSec },
|
|
989
|
+
conformAction: r.conformAction,
|
|
990
|
+
exceedAction: r.exceedAction,
|
|
991
|
+
enforceOnKey: r.enforceOnKey,
|
|
992
|
+
},
|
|
993
|
+
};
|
|
994
|
+
let proxyPolicyCreated = false;
|
|
762
995
|
const puts: string[] = [];
|
|
763
996
|
const { deps, lines } = upgradeDeps(events, {
|
|
764
997
|
overrides: [
|
|
765
998
|
[/^compute url-maps list/, ok(JSON.stringify([urlMap]))],
|
|
766
999
|
[/^compute security-policies list/, ok("caelo-production-waf-1\n")],
|
|
767
|
-
[
|
|
1000
|
+
[
|
|
1001
|
+
/^compute security-policies describe caelo-production-mcp-auth-proxy-armor /,
|
|
1002
|
+
() =>
|
|
1003
|
+
proxyPolicyCreated
|
|
1004
|
+
? ok(JSON.stringify({ rules: [] }))
|
|
1005
|
+
: fail("ERROR: The resource 'caelo-production-mcp-auth-proxy-armor' was not found"),
|
|
1006
|
+
],
|
|
1007
|
+
[
|
|
1008
|
+
/^compute security-policies create caelo-production-mcp-auth-proxy-armor /,
|
|
1009
|
+
() => {
|
|
1010
|
+
proxyPolicyCreated = true;
|
|
1011
|
+
return ok();
|
|
1012
|
+
},
|
|
1013
|
+
],
|
|
1014
|
+
[
|
|
1015
|
+
/^compute security-policies describe caelo-production-waf-1 /,
|
|
1016
|
+
ok(JSON.stringify({ rules: [gatewayMcpRule] })),
|
|
1017
|
+
],
|
|
1018
|
+
[/^compute (network-endpoint-groups|backend-services) describe/, fail("NOT_FOUND")],
|
|
768
1019
|
],
|
|
769
1020
|
});
|
|
770
1021
|
const recordingHttp: HttpFetch = async (url, init) => {
|
|
771
|
-
if (init.method === "PUT" && url.includes("/urlMaps/"))
|
|
772
|
-
puts.push(`${url} ${init.body ?? ""}`);
|
|
1022
|
+
if (init.method === "PUT" && url.includes("/urlMaps/")) puts.push(init.body ?? "");
|
|
773
1023
|
return http(url, init);
|
|
774
1024
|
};
|
|
775
1025
|
expect(await upgradeGcpInstall(GCP_TARGET, { ...deps, http: recordingHttp })).toBe(true);
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
),
|
|
1026
|
+
const armor = indexOf(
|
|
1027
|
+
events,
|
|
1028
|
+
/^gcloud compute security-policies rules create 900 --security-policy=caelo-production-mcp-auth-proxy-armor /,
|
|
780
1029
|
);
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1030
|
+
const neg = indexOf(
|
|
1031
|
+
events,
|
|
1032
|
+
/^gcloud compute network-endpoint-groups create caelo-production-mcp-auth-proxy-neg .*--cloud-run-service=caelo-production-mcp-auth-proxy/,
|
|
1033
|
+
);
|
|
1034
|
+
const backend = indexOf(
|
|
1035
|
+
events,
|
|
1036
|
+
/^gcloud compute backend-services create caelo-production-mcp-auth-proxy-backend .*--security-policy=caelo-production-mcp-auth-proxy-armor/,
|
|
1037
|
+
);
|
|
1038
|
+
const retire = indexOf(
|
|
1039
|
+
events,
|
|
1040
|
+
/^gcloud compute security-policies rules delete 900 --security-policy=caelo-production-waf-1 /,
|
|
787
1041
|
);
|
|
1042
|
+
expect(puts).toHaveLength(1);
|
|
1043
|
+
const put = JSON.parse(puts[0] as string) as typeof urlMap;
|
|
1044
|
+
const rules = put.pathMatchers[0]?.pathRules ?? [];
|
|
1045
|
+
expect(rules).toEqual([
|
|
1046
|
+
{ paths: ["/api/*"], service: gatewayBackend },
|
|
1047
|
+
{
|
|
1048
|
+
paths: [
|
|
1049
|
+
"/api/mcp/*",
|
|
1050
|
+
"/.well-known/oauth-protected-resource/*",
|
|
1051
|
+
"/.well-known/oauth-authorization-server/*",
|
|
1052
|
+
],
|
|
1053
|
+
service:
|
|
1054
|
+
"https://www.googleapis.com/compute/v1/projects/acme/global/backendServices/caelo-production-mcp-auth-proxy-backend",
|
|
1055
|
+
},
|
|
1056
|
+
]);
|
|
1057
|
+
expect(lines).toContain("done: Remote-MCP load-balancer route ready");
|
|
1058
|
+
// Proxy policy before its backend, the old gateway rule only once the
|
|
1059
|
+
// URL map no longer sends MCP there; all before the migrations.
|
|
1060
|
+
expect(armor).toBeLessThan(backend);
|
|
1061
|
+
expect(neg).toBeLessThan(backend);
|
|
1062
|
+
expect(backend).toBeLessThan(retire);
|
|
1063
|
+
expect(retire).toBeLessThan(indexOf(events, /^migrate /));
|
|
788
1064
|
});
|
|
789
1065
|
|
|
790
1066
|
it("on gcp-firebase there is no URL map: no redirects role", async () => {
|
|
@@ -809,7 +1085,11 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
809
1085
|
indexOf(events, /^provisioner identity/),
|
|
810
1086
|
);
|
|
811
1087
|
expect(events.some((e) => e.startsWith("migrate"))).toBe(false);
|
|
812
|
-
expect(
|
|
1088
|
+
expect(
|
|
1089
|
+
calls.filter(
|
|
1090
|
+
(c) => (CONVERGE_WRITE.test(c) && !PRE_PLAN_WRITE.test(c)) || APP_ROLL_WRITE.test(c),
|
|
1091
|
+
),
|
|
1092
|
+
).toEqual([]);
|
|
813
1093
|
});
|
|
814
1094
|
|
|
815
1095
|
it("a deep health probe that throws still rolls back", async () => {
|
|
@@ -827,7 +1107,7 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
827
1107
|
healthProbe: async () => ({ ok: true, probe: throwing }),
|
|
828
1108
|
}),
|
|
829
1109
|
).toBe(false);
|
|
830
|
-
expect(events.filter((e) => e.includes("--to-revisions"))).toHaveLength(
|
|
1110
|
+
expect(events.filter((e) => e.includes("--to-revisions"))).toHaveLength(3);
|
|
831
1111
|
expect(recorded.digests).toEqual([]);
|
|
832
1112
|
});
|
|
833
1113
|
|
|
@@ -868,7 +1148,7 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
868
1148
|
// Its own update: the image roll does not carry it (no new revision).
|
|
869
1149
|
expect(events.filter((e) => e.includes("--ingress=all"))).toHaveLength(1);
|
|
870
1150
|
expect(lines).toContain(
|
|
871
|
-
`info: applied: ingress internal-and-cloud-load-balancing → all on ${GATEWAY} (the gcp-firebase stack's gateway ingress (#650);
|
|
1151
|
+
`info: applied: ingress internal-and-cloud-load-balancing → all on ${GATEWAY} (the gcp-firebase stack's gateway ingress (#650); Firebase Hosting and the health check reach its run.app URL)`,
|
|
872
1152
|
);
|
|
873
1153
|
});
|
|
874
1154
|
|
|
@@ -919,6 +1199,7 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
919
1199
|
domain: "acme.com",
|
|
920
1200
|
adminServiceUrl: `https://${ADMIN}-xyz.a.run.app`,
|
|
921
1201
|
gatewayServiceUrl: `https://${GATEWAY}-xyz.a.run.app`,
|
|
1202
|
+
mcpAuthProxyServiceUrl: `https://${PROXY}-xyz.a.run.app`,
|
|
922
1203
|
},
|
|
923
1204
|
{ run: deps.run, http: htmlGateway, sleep: noSleep, accessJustChanged: false },
|
|
924
1205
|
),
|
|
@@ -987,7 +1268,35 @@ describe("app-roll and converge stay on their side", () => {
|
|
|
987
1268
|
);
|
|
988
1269
|
expect(result).toEqual({ ok: true, digests: DIGEST });
|
|
989
1270
|
expect(calls.filter((c) => CONVERGE_WRITE.test(c))).toEqual([]);
|
|
990
|
-
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(
|
|
1271
|
+
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(3);
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
it("#1338: without a converge, a plan whose services dial the database's IP stops before migrations", async () => {
|
|
1275
|
+
const events: string[] = [];
|
|
1276
|
+
const { run, calls } = fakeInstall(events);
|
|
1277
|
+
const plan = { ...(await plannedRoll(run)), databaseHostPending: true };
|
|
1278
|
+
const { reporter, lines } = recordingReporter();
|
|
1279
|
+
let migrated = false;
|
|
1280
|
+
const result = await executeAppRoll(
|
|
1281
|
+
{ plan, backup: BACKUP, targetVersion: "0.11.0", migrationsContract: false },
|
|
1282
|
+
{},
|
|
1283
|
+
{
|
|
1284
|
+
run,
|
|
1285
|
+
sleep: noSleep,
|
|
1286
|
+
reporter,
|
|
1287
|
+
migrate: async () => {
|
|
1288
|
+
migrated = true;
|
|
1289
|
+
return { ok: true };
|
|
1290
|
+
},
|
|
1291
|
+
health: fakeHealth(events),
|
|
1292
|
+
resolveOpsImage: async () => `registry/ops@sha256:${"c".repeat(64)}`,
|
|
1293
|
+
recordDigests: async () => {},
|
|
1294
|
+
},
|
|
1295
|
+
);
|
|
1296
|
+
expect(result).toEqual({ ok: false, phase: "migrations", migrated: false, rolledBack: false });
|
|
1297
|
+
expect(migrated).toBe(false);
|
|
1298
|
+
expect(calls.filter((c) => APP_ROLL_WRITE.test(c))).toEqual([]);
|
|
1299
|
+
expect(lines.join("\n")).toContain("verify-full TLS");
|
|
991
1300
|
});
|
|
992
1301
|
|
|
993
1302
|
it("converge alone never rolls a service or runs migrations", async () => {
|