@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
|
@@ -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,11 +109,31 @@ 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",
|
|
93
114
|
},
|
|
94
115
|
secretRef("PUBLIC_DATABASE_PASSWORD", "public-role-password"),
|
|
95
116
|
]);
|
|
96
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",
|
|
128
|
+
},
|
|
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" },
|
|
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);
|
|
136
|
+
|
|
97
137
|
type Answer = GcloudResult | ((line: string) => GcloudResult);
|
|
98
138
|
|
|
99
139
|
/**
|
|
@@ -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`);
|
|
@@ -364,6 +423,66 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
364
423
|
).toBe(false);
|
|
365
424
|
});
|
|
366
425
|
|
|
426
|
+
it("#1140 — sets up the scheduler tick before operator access keeps its account on IAP, and tells the admin whom to trust", async () => {
|
|
427
|
+
const events: string[] = [];
|
|
428
|
+
const { deps, lines } = upgradeDeps(events);
|
|
429
|
+
expect(await upgradeGcpInstall({ ...TARGET, schedulerIntervalMinutes: 2 }, deps)).toBe(true);
|
|
430
|
+
const workflow = indexOf(events, /^gcloud workflows deploy caelo-production-scheduler-tick /);
|
|
431
|
+
const schedule = indexOf(
|
|
432
|
+
events,
|
|
433
|
+
/^gcloud scheduler jobs (create|update) http caelo-production-scheduler-tick /,
|
|
434
|
+
);
|
|
435
|
+
const opAccess = indexOf(
|
|
436
|
+
events,
|
|
437
|
+
/^gcloud run jobs deploy caelo-production-operator-access-sync /,
|
|
438
|
+
);
|
|
439
|
+
const adminRoll = indexOf(events, new RegExp(`^gcloud run services update ${ADMIN} `));
|
|
440
|
+
expect(workflow < schedule && schedule < opAccess && opAccess < adminRoll).toBe(true);
|
|
441
|
+
expect(events[schedule]).toContain("--schedule=*/2 * * * *");
|
|
442
|
+
expect(events[schedule]).toContain(
|
|
443
|
+
"--oauth-service-account-email=caelo-production-scheduler@acme.iam.gserviceaccount.com",
|
|
444
|
+
);
|
|
445
|
+
expect(events[opAccess]).toContain(
|
|
446
|
+
"CAELO_OPERATOR_ACCESS_IAP_SERVICE_ACCOUNTS=caelo-production-scheduler@acme.iam.gserviceaccount.com",
|
|
447
|
+
);
|
|
448
|
+
expect(events[adminRoll]).toContain(
|
|
449
|
+
"CAELO_SCHEDULER_SERVICE_ACCOUNT=caelo-production-scheduler@acme.iam.gserviceaccount.com",
|
|
450
|
+
);
|
|
451
|
+
expect(lines).toContain("done: Scheduler tick ready (every 2 min)");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("#1140 — a failed scheduler setup leaves the converge incomplete and the admin's trust env untouched", async () => {
|
|
455
|
+
const events: string[] = [];
|
|
456
|
+
const { deps, recorded, lines } = upgradeDeps(events, {
|
|
457
|
+
overrides: [
|
|
458
|
+
[/^workflows deploy caelo-production-scheduler-tick/, fail("PERMISSION_DENIED")],
|
|
459
|
+
[/^iam service-accounts describe caelo-production-scheduler@/, fail("NOT_FOUND")],
|
|
460
|
+
],
|
|
461
|
+
});
|
|
462
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
463
|
+
expect(recorded.contract).toEqual([]);
|
|
464
|
+
expect(lines.some((l) => l.includes("Scheduler tick not set up"))).toBe(true);
|
|
465
|
+
expect(
|
|
466
|
+
events.some((e) =>
|
|
467
|
+
/services update caelo-production-admin.*CAELO_SCHEDULER_SERVICE_ACCOUNT=/.test(e),
|
|
468
|
+
),
|
|
469
|
+
).toBe(false);
|
|
470
|
+
expect(
|
|
471
|
+
events.some((e) =>
|
|
472
|
+
/^gcloud scheduler jobs (create|update) http caelo-production-scheduler-tick/.test(e),
|
|
473
|
+
),
|
|
474
|
+
).toBe(false);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("#1140 — refuses an out-of-range interval in install.json without touching the scheduler", async () => {
|
|
478
|
+
const events: string[] = [];
|
|
479
|
+
const { deps, recorded, lines } = upgradeDeps(events);
|
|
480
|
+
expect(await upgradeGcpInstall({ ...TARGET, schedulerIntervalMinutes: 60 }, deps)).toBe(true);
|
|
481
|
+
expect(recorded.contract).toEqual([]);
|
|
482
|
+
expect(lines.some((l) => l.includes("between 1 and 15"))).toBe(true);
|
|
483
|
+
expect(events.some((e) => /^gcloud workflows deploy/.test(e))).toBe(false);
|
|
484
|
+
});
|
|
485
|
+
|
|
367
486
|
it("an unreadable audit job is reported as an error, not as a missing job, and is not moved", async () => {
|
|
368
487
|
const events: string[] = [];
|
|
369
488
|
const { deps, lines } = upgradeDeps(events, {
|
|
@@ -462,7 +581,8 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
462
581
|
});
|
|
463
582
|
expect(await upgradeGcpInstall(TARGET, deps)).toBe(false);
|
|
464
583
|
|
|
465
|
-
|
|
584
|
+
// Only the additive pre-plan grants (#1328) came before it.
|
|
585
|
+
const writes = calls.filter((c) => ANY_WRITE.test(c) && !PRE_PLAN_WRITE.test(c));
|
|
466
586
|
expect(writes).toEqual([
|
|
467
587
|
expect.stringMatching(/^sql backups create --instance caelo-production-pg-abc/),
|
|
468
588
|
]);
|
|
@@ -485,7 +605,12 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
485
605
|
const rollbacks = events
|
|
486
606
|
.slice(deep)
|
|
487
607
|
.filter((e) => e.includes("update-traffic") && e.includes("--to-revisions"));
|
|
608
|
+
// Newest first: the proxy (back to the gateway release it was), the
|
|
609
|
+
// gateway, the admin.
|
|
488
610
|
expect(rollbacks).toEqual([
|
|
611
|
+
expect.stringContaining(
|
|
612
|
+
`${PROXY} --region europe-west1 --project acme --to-revisions ${PROXY}-00001-old=100`,
|
|
613
|
+
),
|
|
489
614
|
expect.stringContaining(
|
|
490
615
|
`${GATEWAY} --region europe-west1 --project acme --to-revisions ${GATEWAY}-00001-old=100`,
|
|
491
616
|
),
|
|
@@ -624,7 +749,11 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
624
749
|
const { deps, lines } = upgradeDeps(events, {
|
|
625
750
|
deep: { ok: false, error: "unhealthy" },
|
|
626
751
|
});
|
|
627
|
-
const before = {
|
|
752
|
+
const before = {
|
|
753
|
+
admin: `${ADMIN}-00000-before`,
|
|
754
|
+
gateway: `${GATEWAY}-00000-before`,
|
|
755
|
+
"mcp-auth-proxy": `${PROXY}-00000-before`,
|
|
756
|
+
};
|
|
628
757
|
expect(
|
|
629
758
|
await upgradeGcpInstall(TARGET, {
|
|
630
759
|
...deps,
|
|
@@ -642,6 +771,27 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
642
771
|
);
|
|
643
772
|
const rollbacks = events.filter((e) => e.includes("--to-revisions"));
|
|
644
773
|
expect(rollbacks).toEqual([
|
|
774
|
+
expect.stringContaining(`--to-revisions ${before["mcp-auth-proxy"]}=100`),
|
|
775
|
+
expect.stringContaining(`--to-revisions ${before.gateway}=100`),
|
|
776
|
+
expect.stringContaining(`--to-revisions ${before.admin}=100`),
|
|
777
|
+
]);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it("#1328 — a checkpoint without the proxy's revision (an older release wrote it) keeps the planned one", async () => {
|
|
781
|
+
const events: string[] = [];
|
|
782
|
+
const { deps } = upgradeDeps(events, { deep: { ok: false, error: "unhealthy" } });
|
|
783
|
+
const before = { admin: `${ADMIN}-00000-before`, gateway: `${GATEWAY}-00000-before` };
|
|
784
|
+
expect(
|
|
785
|
+
await upgradeGcpInstall(TARGET, {
|
|
786
|
+
...deps,
|
|
787
|
+
resume: {
|
|
788
|
+
backup: { projectId: "acme", instance: SQL, backupId: "1759999999999" },
|
|
789
|
+
priorRevisions: before,
|
|
790
|
+
},
|
|
791
|
+
}),
|
|
792
|
+
).toBe(false);
|
|
793
|
+
expect(events.filter((e) => e.includes("--to-revisions"))).toEqual([
|
|
794
|
+
expect.stringContaining(`--to-revisions ${PROXY}-00001-old=100`),
|
|
645
795
|
expect.stringContaining(`--to-revisions ${before.gateway}=100`),
|
|
646
796
|
expect.stringContaining(`--to-revisions ${before.admin}=100`),
|
|
647
797
|
]);
|
|
@@ -658,10 +808,33 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
658
808
|
expect(indexOf(events, /^gcloud sql backups create/)).toBeLessThan(revoke);
|
|
659
809
|
expect(revoke).toBeLessThan(indexOf(events, /^migrate /));
|
|
660
810
|
expect(lines).toContain(
|
|
661
|
-
"done:
|
|
811
|
+
"done: The gateway logins hold no database roles (public_role and public_staging_role hold no database roles on caelo-production-pg-abc)",
|
|
662
812
|
);
|
|
663
813
|
});
|
|
664
814
|
|
|
815
|
+
it("creates the staging database and its login before the revoke and the migrations (#1329)", async () => {
|
|
816
|
+
const events: string[] = [];
|
|
817
|
+
const { deps, lines } = upgradeDeps(events);
|
|
818
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
819
|
+
const created = indexOf(events, /^gcloud sql databases (list|create)/);
|
|
820
|
+
const revoke = indexOf(events, /^gcloud sql users assign-roles public_staging_role/);
|
|
821
|
+
expect(created).toBeGreaterThan(-1);
|
|
822
|
+
expect(created).toBeLessThan(revoke);
|
|
823
|
+
expect(revoke).toBeLessThan(indexOf(events, /^migrate /));
|
|
824
|
+
expect(lines).toContain("done: Staging database and its gateway login ready");
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it("a staging database that cannot be created stops the upgrade before migrations and any roll (#1329)", async () => {
|
|
828
|
+
const events: string[] = [];
|
|
829
|
+
const { deps, calls, lines } = upgradeDeps(events, {
|
|
830
|
+
overrides: [[/^sql databases list/, fail("PERMISSION_DENIED: no")]],
|
|
831
|
+
});
|
|
832
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(false);
|
|
833
|
+
expect(events.some((e) => e.startsWith("migrate"))).toBe(false);
|
|
834
|
+
expect(calls.filter((c) => APP_ROLL_WRITE.test(c))).toEqual([]);
|
|
835
|
+
expect(lines).toContain("failed: The staging database could not be set up. Aborting upgrade.");
|
|
836
|
+
});
|
|
837
|
+
|
|
665
838
|
it("a refused public_role revoke stops the upgrade before migrations and any roll (#633)", async () => {
|
|
666
839
|
const events: string[] = [];
|
|
667
840
|
const { deps, calls, lines } = upgradeDeps(events, {
|
|
@@ -671,7 +844,7 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
671
844
|
expect(events.some((e) => e.startsWith("migrate"))).toBe(false);
|
|
672
845
|
expect(calls.filter((c) => APP_ROLL_WRITE.test(c))).toEqual([]);
|
|
673
846
|
expect(lines).toContain(
|
|
674
|
-
"failed:
|
|
847
|
+
"failed: The gateway logins' role memberships could not be revoked. Aborting upgrade.",
|
|
675
848
|
);
|
|
676
849
|
});
|
|
677
850
|
|
|
@@ -714,7 +887,7 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
714
887
|
expect(recorded.contract).toEqual([]);
|
|
715
888
|
});
|
|
716
889
|
|
|
717
|
-
it("#806 — the rolls carry remote MCP: identity env, the
|
|
890
|
+
it("#806/#1328 — the rolls carry remote MCP: identity env, the proxy's client file and timeout", async () => {
|
|
718
891
|
const events: string[] = [];
|
|
719
892
|
const { deps, calls } = upgradeDeps(events);
|
|
720
893
|
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
@@ -723,19 +896,129 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
723
896
|
);
|
|
724
897
|
const admin = rolls.find((c) => c.startsWith(`run services update ${ADMIN} `)) ?? "";
|
|
725
898
|
const gateway = rolls.find((c) => c.startsWith(`run services update ${GATEWAY} `)) ?? "";
|
|
899
|
+
const proxy = rolls.find((c) => c.startsWith(`run services update ${PROXY} `)) ?? "";
|
|
726
900
|
expect(admin).toContain(
|
|
727
901
|
`CAELO_IAP_AUDIENCE=/projects/42/locations/europe-west1/services/${ADMIN}`,
|
|
728
902
|
);
|
|
729
|
-
|
|
730
|
-
expect(
|
|
731
|
-
|
|
732
|
-
expect(
|
|
733
|
-
|
|
903
|
+
// E13 a: the MCP address stays the run.app URL of the service that was the gateway.
|
|
904
|
+
expect(admin).toContain(`CAELO_MCP_PUBLIC_URL=https://${PROXY}-xyz.a.run.app`);
|
|
905
|
+
// The Firebase rewrite follows the data gateway.
|
|
906
|
+
expect(admin).toContain(`CAELO_GATEWAY_SERVICE=${GATEWAY}`);
|
|
907
|
+
expect(proxy).toContain(`--image ${IMAGES["mcp-auth-proxy"].imageRef}`);
|
|
908
|
+
expect(proxy).toContain(`CAELO_ADMIN_URL=https://${ADMIN}-xyz.a.run.app`);
|
|
909
|
+
expect(proxy).toContain(`CAELO_MCP_PUBLIC_URL=https://${PROXY}-xyz.a.run.app`);
|
|
910
|
+
expect(proxy).toContain(
|
|
911
|
+
"/secrets/operator-oauth-client/client.json=caelo-production-operator-oauth-client:latest",
|
|
734
912
|
);
|
|
735
|
-
expect(
|
|
913
|
+
expect(proxy).toContain("--timeout=3600");
|
|
914
|
+
// The gateway carries none of it any more.
|
|
915
|
+
expect(gateway).not.toContain("CAELO_MCP_PUBLIC_URL=");
|
|
916
|
+
expect(gateway).not.toContain("operator-oauth-client");
|
|
917
|
+
expect(gateway).not.toContain("--timeout");
|
|
736
918
|
expect(admin).not.toContain("--timeout");
|
|
737
919
|
// No extra revision: everything rides the image rolls.
|
|
738
|
-
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(
|
|
920
|
+
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(3);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it("#1328 — the gcp-firebase proxy roll leaves the VPC, drops every database login and forwards to the data gateway", async () => {
|
|
924
|
+
const events: string[] = [];
|
|
925
|
+
const { deps, calls } = upgradeDeps(events);
|
|
926
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
927
|
+
const proxy =
|
|
928
|
+
calls.find((c) => new RegExp(`^run services update ${PROXY} .*--image`).test(c)) ?? "";
|
|
929
|
+
expect(proxy).toContain(
|
|
930
|
+
"--service-account=caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com",
|
|
931
|
+
);
|
|
932
|
+
expect(proxy).toContain("--clear-network");
|
|
933
|
+
expect(proxy).toContain("CAELO_REMOTE_MCP=available");
|
|
934
|
+
expect(proxy).toContain(`CAELO_INTERIM_FORWARD_ORIGIN=https://${GATEWAY}-xyz.a.run.app`);
|
|
935
|
+
const removedEnv = proxy.match(/--remove-env-vars=(\S+)/)?.[1]?.split(",") ?? [];
|
|
936
|
+
const removedSecrets = proxy.match(/--remove-secrets=(\S+)/)?.[1]?.split(",") ?? [];
|
|
937
|
+
expect(removedEnv).toContain("PUBLIC_DATABASE_URL");
|
|
938
|
+
expect(removedSecrets).toEqual(
|
|
939
|
+
expect.arrayContaining(["PUBLIC_DATABASE_PASSWORD", "CAELO_STAGING_GATE_SECRET"]),
|
|
940
|
+
);
|
|
941
|
+
// The gateway keeps its VPC route to Cloud SQL.
|
|
942
|
+
const gateway =
|
|
943
|
+
calls.find((c) => new RegExp(`^run services update ${GATEWAY} .*--image`).test(c)) ?? "";
|
|
944
|
+
expect(gateway).not.toContain("--clear-network");
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it("#1328 — gcp-firebase: the pre-plan step copies the data gateway from the old gateway when it is missing", async () => {
|
|
948
|
+
const events: string[] = [];
|
|
949
|
+
const { deps, calls, lines } = upgradeDeps(events, {
|
|
950
|
+
overrides: [
|
|
951
|
+
[
|
|
952
|
+
new RegExp(`^run services describe ${GATEWAY} .*metadata\\.name`),
|
|
953
|
+
fail(`ERROR: Cannot find service [${GATEWAY}]. NOT_FOUND`),
|
|
954
|
+
],
|
|
955
|
+
],
|
|
956
|
+
});
|
|
957
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
958
|
+
const replace = indexOf(events, /^gcloud run services replace /);
|
|
959
|
+
expect(replace).toBeLessThan(indexOf(events, /^gcloud sql backups create/));
|
|
960
|
+
expect(lines).toContain(
|
|
961
|
+
`info: created: Cloud Run service ${GATEWAY} (the visitor gateway) (copied from ${PROXY})`,
|
|
962
|
+
);
|
|
963
|
+
// Nothing else is deployed before the plan: the proxy is rolled later.
|
|
964
|
+
expect(calls.some((c) => /^run deploy /.test(c))).toBe(false);
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
it("#1328 — gcp-firebase: an existing data gateway makes the pre-plan step a no-op", async () => {
|
|
968
|
+
const events: string[] = [];
|
|
969
|
+
const { deps, calls } = upgradeDeps(events);
|
|
970
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
971
|
+
expect(calls.some((c) => /^run services replace |^run deploy /.test(c))).toBe(false);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it("#1328 — a failed pre-plan step stops before the backup, with nothing rolled", async () => {
|
|
975
|
+
const events: string[] = [];
|
|
976
|
+
const { deps, calls, lines } = upgradeDeps(events, {
|
|
977
|
+
overrides: [[/^secrets add-iam-policy-binding/, fail("PERMISSION_DENIED: setIamPolicy")]],
|
|
978
|
+
});
|
|
979
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(false);
|
|
980
|
+
expect(calls.some((c) => /^sql backups create/.test(c))).toBe(false);
|
|
981
|
+
expect(calls.filter((c) => APP_ROLL_WRITE.test(c))).toEqual([]);
|
|
982
|
+
expect(events.some((e) => e.startsWith("migrate"))).toBe(false);
|
|
983
|
+
expect(
|
|
984
|
+
lines.some((l) => l.startsWith("failed: The services this release runs could not be set up")),
|
|
985
|
+
).toBe(true);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it("#1328 — gcp: the proxy is deployed only after its image's signature verifies", async () => {
|
|
989
|
+
const missingProxy: [RegExp, Answer] = [
|
|
990
|
+
new RegExp(`^run services describe ${GCP_PROXY} .*metadata\\.name`),
|
|
991
|
+
fail(`ERROR: Cannot find service [${GCP_PROXY}]. NOT_FOUND`),
|
|
992
|
+
];
|
|
993
|
+
const events: string[] = [];
|
|
994
|
+
const { deps } = upgradeDeps(events, { overrides: [missingProxy] });
|
|
995
|
+
expect(
|
|
996
|
+
await upgradeGcpInstall(GCP_TARGET, {
|
|
997
|
+
...deps,
|
|
998
|
+
verifyImages: async (images) => {
|
|
999
|
+
events.push(`verify ${images.map((i) => i.slug).join(",")}`);
|
|
1000
|
+
return true;
|
|
1001
|
+
},
|
|
1002
|
+
}),
|
|
1003
|
+
).toBe(true);
|
|
1004
|
+
const verify = indexOf(events, /^verify mcp-auth-proxy$/);
|
|
1005
|
+
const deploy = indexOf(events, new RegExp(`^gcloud run deploy ${GCP_PROXY} `));
|
|
1006
|
+
expect(verify).toBeLessThan(deploy);
|
|
1007
|
+
expect(events[deploy]).toContain(`--image=${IMAGES["mcp-auth-proxy"].imageRef}`);
|
|
1008
|
+
expect(events[deploy]).toContain("--ingress=internal-and-cloud-load-balancing");
|
|
1009
|
+
expect(deploy).toBeLessThan(indexOf(events, /^gcloud sql backups create/));
|
|
1010
|
+
|
|
1011
|
+
// A signature that does not verify: nothing is deployed, nothing backed up.
|
|
1012
|
+
const refused: string[] = [];
|
|
1013
|
+
const bad = upgradeDeps(refused, { overrides: [missingProxy] });
|
|
1014
|
+
expect(
|
|
1015
|
+
await upgradeGcpInstall(GCP_TARGET, {
|
|
1016
|
+
...bad.deps,
|
|
1017
|
+
verifyImages: async (images) => !images.some((i) => i.slug === "mcp-auth-proxy"),
|
|
1018
|
+
}),
|
|
1019
|
+
).toBe(false);
|
|
1020
|
+
expect(bad.calls.some((c) => /^run deploy /.test(c))).toBe(false);
|
|
1021
|
+
expect(bad.calls.some((c) => /^sql backups create/.test(c))).toBe(false);
|
|
739
1022
|
});
|
|
740
1023
|
|
|
741
1024
|
it("moves the gateway to request-based CPU in its image roll; the admin keeps its CPU", async () => {
|
|
@@ -747,44 +1030,120 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
747
1030
|
);
|
|
748
1031
|
const admin = rolls.find((c) => c.startsWith(`run services update ${ADMIN} `)) ?? "";
|
|
749
1032
|
const gateway = rolls.find((c) => c.startsWith(`run services update ${GATEWAY} `)) ?? "";
|
|
1033
|
+
const proxy = rolls.find((c) => c.startsWith(`run services update ${PROXY} `)) ?? "";
|
|
750
1034
|
expect(gateway).toContain("--cpu-throttling");
|
|
1035
|
+
expect(proxy).toContain("--cpu-throttling");
|
|
751
1036
|
expect(admin).not.toContain("cpu-throttling");
|
|
752
|
-
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(
|
|
1037
|
+
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(3);
|
|
753
1038
|
});
|
|
754
1039
|
|
|
755
|
-
it("#806 — on gcp,
|
|
1040
|
+
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
1041
|
const events: string[] = [];
|
|
1042
|
+
// The #806 shape: the discovery paths on the gateway's rule, the MCP
|
|
1043
|
+
// rule on the gateway's policy.
|
|
1044
|
+
const gatewayBackend =
|
|
1045
|
+
"https://www.googleapis.com/compute/v1/projects/acme/global/backendServices/caelo-production-gateway-backend-1";
|
|
757
1046
|
const urlMap = {
|
|
758
1047
|
name: "caelo-production-url-map-1",
|
|
759
1048
|
hostRules: [{ hosts: ["acme.com"], pathMatcher: "public" }],
|
|
760
|
-
pathMatchers: [
|
|
1049
|
+
pathMatchers: [
|
|
1050
|
+
{
|
|
1051
|
+
name: "public",
|
|
1052
|
+
pathRules: [
|
|
1053
|
+
{
|
|
1054
|
+
paths: [
|
|
1055
|
+
"/api/*",
|
|
1056
|
+
"/.well-known/oauth-protected-resource/*",
|
|
1057
|
+
"/.well-known/oauth-authorization-server/*",
|
|
1058
|
+
],
|
|
1059
|
+
service: gatewayBackend,
|
|
1060
|
+
},
|
|
1061
|
+
],
|
|
1062
|
+
},
|
|
1063
|
+
],
|
|
1064
|
+
};
|
|
1065
|
+
const r = MCP_ARMOR_RULE;
|
|
1066
|
+
const gatewayMcpRule = {
|
|
1067
|
+
priority: r.priority,
|
|
1068
|
+
action: r.action,
|
|
1069
|
+
match: { expr: { expression: r.expression } },
|
|
1070
|
+
rateLimitOptions: {
|
|
1071
|
+
rateLimitThreshold: { count: r.count, intervalSec: r.intervalSec },
|
|
1072
|
+
conformAction: r.conformAction,
|
|
1073
|
+
exceedAction: r.exceedAction,
|
|
1074
|
+
enforceOnKey: r.enforceOnKey,
|
|
1075
|
+
},
|
|
761
1076
|
};
|
|
1077
|
+
let proxyPolicyCreated = false;
|
|
762
1078
|
const puts: string[] = [];
|
|
763
1079
|
const { deps, lines } = upgradeDeps(events, {
|
|
764
1080
|
overrides: [
|
|
765
1081
|
[/^compute url-maps list/, ok(JSON.stringify([urlMap]))],
|
|
766
1082
|
[/^compute security-policies list/, ok("caelo-production-waf-1\n")],
|
|
767
|
-
[
|
|
1083
|
+
[
|
|
1084
|
+
/^compute security-policies describe caelo-production-mcp-auth-proxy-armor /,
|
|
1085
|
+
() =>
|
|
1086
|
+
proxyPolicyCreated
|
|
1087
|
+
? ok(JSON.stringify({ rules: [] }))
|
|
1088
|
+
: fail("ERROR: The resource 'caelo-production-mcp-auth-proxy-armor' was not found"),
|
|
1089
|
+
],
|
|
1090
|
+
[
|
|
1091
|
+
/^compute security-policies create caelo-production-mcp-auth-proxy-armor /,
|
|
1092
|
+
() => {
|
|
1093
|
+
proxyPolicyCreated = true;
|
|
1094
|
+
return ok();
|
|
1095
|
+
},
|
|
1096
|
+
],
|
|
1097
|
+
[
|
|
1098
|
+
/^compute security-policies describe caelo-production-waf-1 /,
|
|
1099
|
+
ok(JSON.stringify({ rules: [gatewayMcpRule] })),
|
|
1100
|
+
],
|
|
1101
|
+
[/^compute (network-endpoint-groups|backend-services) describe/, fail("NOT_FOUND")],
|
|
768
1102
|
],
|
|
769
1103
|
});
|
|
770
1104
|
const recordingHttp: HttpFetch = async (url, init) => {
|
|
771
|
-
if (init.method === "PUT" && url.includes("/urlMaps/"))
|
|
772
|
-
puts.push(`${url} ${init.body ?? ""}`);
|
|
1105
|
+
if (init.method === "PUT" && url.includes("/urlMaps/")) puts.push(init.body ?? "");
|
|
773
1106
|
return http(url, init);
|
|
774
1107
|
};
|
|
775
1108
|
expect(await upgradeGcpInstall(GCP_TARGET, { ...deps, http: recordingHttp })).toBe(true);
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
),
|
|
1109
|
+
const armor = indexOf(
|
|
1110
|
+
events,
|
|
1111
|
+
/^gcloud compute security-policies rules create 900 --security-policy=caelo-production-mcp-auth-proxy-armor /,
|
|
780
1112
|
);
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
expect(lines).toContain("done: Remote-MCP load-balancer rules ready");
|
|
785
|
-
expect(indexOf(events, /security-policies rules create/)).toBeLessThan(
|
|
786
|
-
indexOf(events, /^migrate /),
|
|
1113
|
+
const neg = indexOf(
|
|
1114
|
+
events,
|
|
1115
|
+
/^gcloud compute network-endpoint-groups create caelo-production-mcp-auth-proxy-neg .*--cloud-run-service=caelo-production-mcp-auth-proxy/,
|
|
787
1116
|
);
|
|
1117
|
+
const backend = indexOf(
|
|
1118
|
+
events,
|
|
1119
|
+
/^gcloud compute backend-services create caelo-production-mcp-auth-proxy-backend .*--security-policy=caelo-production-mcp-auth-proxy-armor/,
|
|
1120
|
+
);
|
|
1121
|
+
const retire = indexOf(
|
|
1122
|
+
events,
|
|
1123
|
+
/^gcloud compute security-policies rules delete 900 --security-policy=caelo-production-waf-1 /,
|
|
1124
|
+
);
|
|
1125
|
+
expect(puts).toHaveLength(1);
|
|
1126
|
+
const put = JSON.parse(puts[0] as string) as typeof urlMap;
|
|
1127
|
+
const rules = put.pathMatchers[0]?.pathRules ?? [];
|
|
1128
|
+
expect(rules).toEqual([
|
|
1129
|
+
{ paths: ["/api/*"], service: gatewayBackend },
|
|
1130
|
+
{
|
|
1131
|
+
paths: [
|
|
1132
|
+
"/api/mcp/*",
|
|
1133
|
+
"/.well-known/oauth-protected-resource/*",
|
|
1134
|
+
"/.well-known/oauth-authorization-server/*",
|
|
1135
|
+
],
|
|
1136
|
+
service:
|
|
1137
|
+
"https://www.googleapis.com/compute/v1/projects/acme/global/backendServices/caelo-production-mcp-auth-proxy-backend",
|
|
1138
|
+
},
|
|
1139
|
+
]);
|
|
1140
|
+
expect(lines).toContain("done: Remote-MCP load-balancer route ready");
|
|
1141
|
+
// Proxy policy before its backend, the old gateway rule only once the
|
|
1142
|
+
// URL map no longer sends MCP there; all before the migrations.
|
|
1143
|
+
expect(armor).toBeLessThan(backend);
|
|
1144
|
+
expect(neg).toBeLessThan(backend);
|
|
1145
|
+
expect(backend).toBeLessThan(retire);
|
|
1146
|
+
expect(retire).toBeLessThan(indexOf(events, /^migrate /));
|
|
788
1147
|
});
|
|
789
1148
|
|
|
790
1149
|
it("on gcp-firebase there is no URL map: no redirects role", async () => {
|
|
@@ -809,7 +1168,11 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
809
1168
|
indexOf(events, /^provisioner identity/),
|
|
810
1169
|
);
|
|
811
1170
|
expect(events.some((e) => e.startsWith("migrate"))).toBe(false);
|
|
812
|
-
expect(
|
|
1171
|
+
expect(
|
|
1172
|
+
calls.filter(
|
|
1173
|
+
(c) => (CONVERGE_WRITE.test(c) && !PRE_PLAN_WRITE.test(c)) || APP_ROLL_WRITE.test(c),
|
|
1174
|
+
),
|
|
1175
|
+
).toEqual([]);
|
|
813
1176
|
});
|
|
814
1177
|
|
|
815
1178
|
it("a deep health probe that throws still rolls back", async () => {
|
|
@@ -827,7 +1190,7 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
827
1190
|
healthProbe: async () => ({ ok: true, probe: throwing }),
|
|
828
1191
|
}),
|
|
829
1192
|
).toBe(false);
|
|
830
|
-
expect(events.filter((e) => e.includes("--to-revisions"))).toHaveLength(
|
|
1193
|
+
expect(events.filter((e) => e.includes("--to-revisions"))).toHaveLength(3);
|
|
831
1194
|
expect(recorded.digests).toEqual([]);
|
|
832
1195
|
});
|
|
833
1196
|
|
|
@@ -868,7 +1231,7 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
868
1231
|
// Its own update: the image roll does not carry it (no new revision).
|
|
869
1232
|
expect(events.filter((e) => e.includes("--ingress=all"))).toHaveLength(1);
|
|
870
1233
|
expect(lines).toContain(
|
|
871
|
-
`info: applied: ingress internal-and-cloud-load-balancing → all on ${GATEWAY} (the gcp-firebase stack's gateway ingress (#650);
|
|
1234
|
+
`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
1235
|
);
|
|
873
1236
|
});
|
|
874
1237
|
|
|
@@ -919,6 +1282,7 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
919
1282
|
domain: "acme.com",
|
|
920
1283
|
adminServiceUrl: `https://${ADMIN}-xyz.a.run.app`,
|
|
921
1284
|
gatewayServiceUrl: `https://${GATEWAY}-xyz.a.run.app`,
|
|
1285
|
+
mcpAuthProxyServiceUrl: `https://${PROXY}-xyz.a.run.app`,
|
|
922
1286
|
},
|
|
923
1287
|
{ run: deps.run, http: htmlGateway, sleep: noSleep, accessJustChanged: false },
|
|
924
1288
|
),
|
|
@@ -987,7 +1351,35 @@ describe("app-roll and converge stay on their side", () => {
|
|
|
987
1351
|
);
|
|
988
1352
|
expect(result).toEqual({ ok: true, digests: DIGEST });
|
|
989
1353
|
expect(calls.filter((c) => CONVERGE_WRITE.test(c))).toEqual([]);
|
|
990
|
-
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(
|
|
1354
|
+
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(3);
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
it("#1338: without a converge, a plan whose services dial the database's IP stops before migrations", async () => {
|
|
1358
|
+
const events: string[] = [];
|
|
1359
|
+
const { run, calls } = fakeInstall(events);
|
|
1360
|
+
const plan = { ...(await plannedRoll(run)), databaseHostPending: true };
|
|
1361
|
+
const { reporter, lines } = recordingReporter();
|
|
1362
|
+
let migrated = false;
|
|
1363
|
+
const result = await executeAppRoll(
|
|
1364
|
+
{ plan, backup: BACKUP, targetVersion: "0.11.0", migrationsContract: false },
|
|
1365
|
+
{},
|
|
1366
|
+
{
|
|
1367
|
+
run,
|
|
1368
|
+
sleep: noSleep,
|
|
1369
|
+
reporter,
|
|
1370
|
+
migrate: async () => {
|
|
1371
|
+
migrated = true;
|
|
1372
|
+
return { ok: true };
|
|
1373
|
+
},
|
|
1374
|
+
health: fakeHealth(events),
|
|
1375
|
+
resolveOpsImage: async () => `registry/ops@sha256:${"c".repeat(64)}`,
|
|
1376
|
+
recordDigests: async () => {},
|
|
1377
|
+
},
|
|
1378
|
+
);
|
|
1379
|
+
expect(result).toEqual({ ok: false, phase: "migrations", migrated: false, rolledBack: false });
|
|
1380
|
+
expect(migrated).toBe(false);
|
|
1381
|
+
expect(calls.filter((c) => APP_ROLL_WRITE.test(c))).toEqual([]);
|
|
1382
|
+
expect(lines.join("\n")).toContain("verify-full TLS");
|
|
991
1383
|
});
|
|
992
1384
|
|
|
993
1385
|
it("converge alone never rolls a service or runs migrations", async () => {
|