@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.
Files changed (352) hide show
  1. package/dist/adapter.d.ts +6 -0
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/admin-domain.d.ts +10 -1
  4. package/dist/admin-domain.d.ts.map +1 -1
  5. package/dist/admin-domain.js +13 -3
  6. package/dist/admin-domain.js.map +1 -1
  7. package/dist/adopt-existing.d.ts +12 -3
  8. package/dist/adopt-existing.d.ts.map +1 -1
  9. package/dist/adopt-existing.js +143 -0
  10. package/dist/adopt-existing.js.map +1 -1
  11. package/dist/agent/command.d.ts +23 -0
  12. package/dist/agent/command.d.ts.map +1 -1
  13. package/dist/agent/command.js +32 -9
  14. package/dist/agent/command.js.map +1 -1
  15. package/dist/agent/run.d.ts +2 -0
  16. package/dist/agent/run.d.ts.map +1 -1
  17. package/dist/agent/run.js +5 -1
  18. package/dist/agent/run.js.map +1 -1
  19. package/dist/agent-db.js +69 -10
  20. package/dist/aws-task-roles.d.ts +4 -2
  21. package/dist/aws-task-roles.d.ts.map +1 -1
  22. package/dist/aws-task-roles.js +4 -1
  23. package/dist/aws-task-roles.js.map +1 -1
  24. package/dist/caddy.d.ts +21 -4
  25. package/dist/caddy.d.ts.map +1 -1
  26. package/dist/caddy.js +27 -3
  27. package/dist/caddy.js.map +1 -1
  28. package/dist/cli-routing.js +1 -1
  29. package/dist/cli-routing.js.map +1 -1
  30. package/dist/cli.js +68 -7
  31. package/dist/cli.js.map +1 -1
  32. package/dist/cloudsql-tls.d.ts +93 -0
  33. package/dist/cloudsql-tls.d.ts.map +1 -0
  34. package/dist/cloudsql-tls.js +295 -0
  35. package/dist/cloudsql-tls.js.map +1 -0
  36. package/dist/compose.d.ts +43 -6
  37. package/dist/compose.d.ts.map +1 -1
  38. package/dist/compose.js +148 -23
  39. package/dist/compose.js.map +1 -1
  40. package/dist/converge-checkpoint.d.ts +1 -0
  41. package/dist/converge-checkpoint.d.ts.map +1 -1
  42. package/dist/converge-checkpoint.js +8 -1
  43. package/dist/converge-checkpoint.js.map +1 -1
  44. package/dist/converge-db-trust.d.ts +44 -0
  45. package/dist/converge-db-trust.d.ts.map +1 -0
  46. package/dist/converge-db-trust.js +158 -0
  47. package/dist/converge-db-trust.js.map +1 -0
  48. package/dist/converge-job-gcp.d.ts +5 -2
  49. package/dist/converge-job-gcp.d.ts.map +1 -1
  50. package/dist/converge-job-gcp.js +11 -13
  51. package/dist/converge-job-gcp.js.map +1 -1
  52. package/dist/converge-job-preflight.d.ts.map +1 -1
  53. package/dist/converge-job-preflight.js +8 -0
  54. package/dist/converge-job-preflight.js.map +1 -1
  55. package/dist/converge-job.d.ts +17 -0
  56. package/dist/converge-job.d.ts.map +1 -1
  57. package/dist/converge-job.js +12 -0
  58. package/dist/converge-job.js.map +1 -1
  59. package/dist/deployed-release.d.ts +4 -1
  60. package/dist/deployed-release.d.ts.map +1 -1
  61. package/dist/deployed-release.js +18 -8
  62. package/dist/deployed-release.js.map +1 -1
  63. package/dist/gcp-names.d.ts +34 -0
  64. package/dist/gcp-names.d.ts.map +1 -1
  65. package/dist/gcp-names.js +45 -0
  66. package/dist/gcp-names.js.map +1 -1
  67. package/dist/gcp-url-map.d.ts.map +1 -1
  68. package/dist/gcp-url-map.js +6 -2
  69. package/dist/gcp-url-map.js.map +1 -1
  70. package/dist/index.d.ts +3 -2
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/infra-contract.d.ts +1 -1
  75. package/dist/infra-contract.d.ts.map +1 -1
  76. package/dist/infra-contract.js +45 -1
  77. package/dist/infra-contract.js.map +1 -1
  78. package/dist/install-job.d.ts +4 -2
  79. package/dist/install-job.d.ts.map +1 -1
  80. package/dist/install-job.js +4 -1
  81. package/dist/install-job.js.map +1 -1
  82. package/dist/install-state.d.ts +13 -0
  83. package/dist/install-state.d.ts.map +1 -1
  84. package/dist/install-state.js +17 -4
  85. package/dist/install-state.js.map +1 -1
  86. package/dist/lifecycle.d.ts.map +1 -1
  87. package/dist/lifecycle.js +20 -8
  88. package/dist/lifecycle.js.map +1 -1
  89. package/dist/mcp-auth-proxy-setup.d.ts +48 -0
  90. package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
  91. package/dist/mcp-auth-proxy-setup.js +280 -0
  92. package/dist/mcp-auth-proxy-setup.js.map +1 -0
  93. package/dist/migration-runner.d.ts.map +1 -1
  94. package/dist/migration-runner.js +8 -3
  95. package/dist/migration-runner.js.map +1 -1
  96. package/dist/migration-targets.d.ts +11 -0
  97. package/dist/migration-targets.d.ts.map +1 -0
  98. package/dist/migration-targets.js +12 -0
  99. package/dist/migration-targets.js.map +1 -0
  100. package/dist/operator-access.d.ts +10 -0
  101. package/dist/operator-access.d.ts.map +1 -1
  102. package/dist/operator-access.js +5 -0
  103. package/dist/operator-access.js.map +1 -1
  104. package/dist/pool-budget-stack.d.ts +13 -1
  105. package/dist/pool-budget-stack.d.ts.map +1 -1
  106. package/dist/pool-budget-stack.js +11 -1
  107. package/dist/pool-budget-stack.js.map +1 -1
  108. package/dist/public-role-isolation.d.ts +7 -3
  109. package/dist/public-role-isolation.d.ts.map +1 -1
  110. package/dist/public-role-isolation.js +25 -13
  111. package/dist/public-role-isolation.js.map +1 -1
  112. package/dist/release-contract.d.ts.map +1 -1
  113. package/dist/release-contract.js +3 -0
  114. package/dist/release-contract.js.map +1 -1
  115. package/dist/release-manifest.d.ts +25 -1
  116. package/dist/release-manifest.d.ts.map +1 -1
  117. package/dist/release-manifest.js +36 -3
  118. package/dist/release-manifest.js.map +1 -1
  119. package/dist/remote-mcp.d.ts +55 -18
  120. package/dist/remote-mcp.d.ts.map +1 -1
  121. package/dist/remote-mcp.js +307 -77
  122. package/dist/remote-mcp.js.map +1 -1
  123. package/dist/runtime-secrets.d.ts +11 -0
  124. package/dist/runtime-secrets.d.ts.map +1 -1
  125. package/dist/runtime-secrets.js +40 -25
  126. package/dist/runtime-secrets.js.map +1 -1
  127. package/dist/scheduler-setup.d.ts +75 -0
  128. package/dist/scheduler-setup.d.ts.map +1 -0
  129. package/dist/scheduler-setup.js +331 -0
  130. package/dist/scheduler-setup.js.map +1 -0
  131. package/dist/seal-key-rotation.d.ts +7 -7
  132. package/dist/seal-key-rotation.d.ts.map +1 -1
  133. package/dist/seal-key-rotation.js +22 -22
  134. package/dist/seal-key-rotation.js.map +1 -1
  135. package/dist/self-hosted-db-tls.d.ts +37 -0
  136. package/dist/self-hosted-db-tls.d.ts.map +1 -0
  137. package/dist/self-hosted-db-tls.js +220 -0
  138. package/dist/self-hosted-db-tls.js.map +1 -0
  139. package/dist/self-hosted-release.d.ts +3 -1
  140. package/dist/self-hosted-release.d.ts.map +1 -1
  141. package/dist/self-hosted-release.js +33 -9
  142. package/dist/self-hosted-release.js.map +1 -1
  143. package/dist/self-hosted-roles.d.ts +43 -2
  144. package/dist/self-hosted-roles.d.ts.map +1 -1
  145. package/dist/self-hosted-roles.js +87 -1
  146. package/dist/self-hosted-roles.js.map +1 -1
  147. package/dist/self-hosted-scheduler-interval.d.ts +21 -0
  148. package/dist/self-hosted-scheduler-interval.d.ts.map +1 -0
  149. package/dist/self-hosted-scheduler-interval.js +68 -0
  150. package/dist/self-hosted-scheduler-interval.js.map +1 -0
  151. package/dist/self-hosted-start.d.ts +0 -21
  152. package/dist/self-hosted-start.d.ts.map +1 -1
  153. package/dist/self-hosted-start.js +39 -1
  154. package/dist/self-hosted-start.js.map +1 -1
  155. package/dist/self-hosted-upgrade.d.ts +19 -7
  156. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  157. package/dist/self-hosted-upgrade.js +78 -25
  158. package/dist/self-hosted-upgrade.js.map +1 -1
  159. package/dist/stack-contract.d.ts +234 -28
  160. package/dist/stack-contract.d.ts.map +1 -1
  161. package/dist/stack-contract.js +397 -39
  162. package/dist/stack-contract.js.map +1 -1
  163. package/dist/stack-converge.d.ts +31 -0
  164. package/dist/stack-converge.d.ts.map +1 -1
  165. package/dist/stack-converge.js +71 -30
  166. package/dist/stack-converge.js.map +1 -1
  167. package/dist/staging-database.d.ts +22 -0
  168. package/dist/staging-database.d.ts.map +1 -0
  169. package/dist/staging-database.js +117 -0
  170. package/dist/staging-database.js.map +1 -0
  171. package/dist/upgrade/app-roll.d.ts +25 -4
  172. package/dist/upgrade/app-roll.d.ts.map +1 -1
  173. package/dist/upgrade/app-roll.js +63 -17
  174. package/dist/upgrade/app-roll.js.map +1 -1
  175. package/dist/upgrade/converge.d.ts +5 -0
  176. package/dist/upgrade/converge.d.ts.map +1 -1
  177. package/dist/upgrade/converge.js +207 -20
  178. package/dist/upgrade/converge.js.map +1 -1
  179. package/dist/upgrade/health.d.ts +31 -7
  180. package/dist/upgrade/health.d.ts.map +1 -1
  181. package/dist/upgrade/health.js +41 -17
  182. package/dist/upgrade/health.js.map +1 -1
  183. package/dist/upgrade/image-forms.d.ts +1 -1
  184. package/dist/upgrade/image-forms.d.ts.map +1 -1
  185. package/dist/upgrade/image-forms.js +24 -7
  186. package/dist/upgrade/image-forms.js.map +1 -1
  187. package/dist/upgrade/release-images.d.ts +12 -3
  188. package/dist/upgrade/release-images.d.ts.map +1 -1
  189. package/dist/upgrade/release-images.js +13 -4
  190. package/dist/upgrade/release-images.js.map +1 -1
  191. package/dist/upgrade/upgrade.d.ts +19 -3
  192. package/dist/upgrade/upgrade.d.ts.map +1 -1
  193. package/dist/upgrade/upgrade.js +37 -2
  194. package/dist/upgrade/upgrade.js.map +1 -1
  195. package/dist/upgrader-gcp.d.ts +13 -0
  196. package/dist/upgrader-gcp.d.ts.map +1 -1
  197. package/dist/upgrader-gcp.js +50 -1
  198. package/dist/upgrader-gcp.js.map +1 -1
  199. package/dist/upgrader-iam-preflight.d.ts.map +1 -1
  200. package/dist/upgrader-iam-preflight.js +6 -1
  201. package/dist/upgrader-iam-preflight.js.map +1 -1
  202. package/dist/upgrader-job.d.ts +1 -1
  203. package/dist/upgrader-job.d.ts.map +1 -1
  204. package/dist/upgrader-job.js +20 -14
  205. package/dist/upgrader-job.js.map +1 -1
  206. package/dist/upgrader-records.d.ts +1 -1
  207. package/dist/upgrader-records.d.ts.map +1 -1
  208. package/dist/upgrader-records.js +26 -7
  209. package/dist/upgrader-records.js.map +1 -1
  210. package/dist/upgrader-setup.d.ts.map +1 -1
  211. package/dist/upgrader-setup.js +10 -5
  212. package/dist/upgrader-setup.js.map +1 -1
  213. package/dist/wizard.d.ts +7 -0
  214. package/dist/wizard.d.ts.map +1 -1
  215. package/dist/wizard.js +11 -0
  216. package/dist/wizard.js.map +1 -1
  217. package/dist/wizards/gcp-cost.d.ts +31 -0
  218. package/dist/wizards/gcp-cost.d.ts.map +1 -1
  219. package/dist/wizards/gcp-cost.js +103 -18
  220. package/dist/wizards/gcp-cost.js.map +1 -1
  221. package/dist/wizards/gcp.d.ts.map +1 -1
  222. package/dist/wizards/gcp.js +129 -27
  223. package/dist/wizards/gcp.js.map +1 -1
  224. package/package.json +2 -2
  225. package/src/adapter.ts +6 -0
  226. package/src/admin-domain.test.ts +9 -8
  227. package/src/admin-domain.ts +13 -3
  228. package/src/admin-memory.test.ts +2 -1
  229. package/src/adopt-existing.test.ts +85 -0
  230. package/src/adopt-existing.ts +141 -3
  231. package/src/agent/command.test.ts +49 -1
  232. package/src/agent/command.ts +39 -9
  233. package/src/agent/run.test.ts +19 -0
  234. package/src/agent/run.ts +11 -2
  235. package/src/aws-task-roles.ts +5 -2
  236. package/src/caddy-redirects.docker.test.ts +2 -0
  237. package/src/caddy-redirects.test.ts +4 -1
  238. package/src/caddy-validate.test.ts +7 -1
  239. package/src/caddy.test.ts +39 -1
  240. package/src/caddy.ts +36 -4
  241. package/src/checkpoint-secrets.test.ts +14 -6
  242. package/src/cli-routing.test.ts +1 -1
  243. package/src/cli-routing.ts +1 -1
  244. package/src/cli.ts +71 -5
  245. package/src/cloudsql-tls.test.ts +255 -0
  246. package/src/cloudsql-tls.ts +359 -0
  247. package/src/compose.ts +193 -24
  248. package/src/converge-checkpoint.ts +8 -1
  249. package/src/converge-db-trust.test.ts +237 -0
  250. package/src/converge-db-trust.ts +212 -0
  251. package/src/converge-job-gcp.ts +11 -12
  252. package/src/converge-job-preflight.test.ts +5 -0
  253. package/src/converge-job-preflight.ts +8 -0
  254. package/src/converge-job.test.ts +70 -0
  255. package/src/converge-job.ts +32 -0
  256. package/src/database-tls-builders.test.ts +56 -0
  257. package/src/deployed-release.test.ts +107 -40
  258. package/src/deployed-release.ts +30 -9
  259. package/src/gateway-env-isolation.test.ts +50 -12
  260. package/src/gcp-names.ts +56 -0
  261. package/src/gcp-url-map.test.ts +6 -8
  262. package/src/gcp-url-map.ts +6 -2
  263. package/src/index.test.ts +10 -4
  264. package/src/index.ts +7 -0
  265. package/src/infra-contract.test.ts +7 -3
  266. package/src/infra-contract.ts +45 -1
  267. package/src/install-job.ts +4 -1
  268. package/src/install-state.ts +33 -4
  269. package/src/lifecycle.ts +23 -11
  270. package/src/mcp-auth-proxy-setup.test.ts +305 -0
  271. package/src/mcp-auth-proxy-setup.ts +374 -0
  272. package/src/mcp-iap-stacks.test.ts +1 -1
  273. package/src/media-volume.test.ts +1 -1
  274. package/src/migration-runner.ts +14 -9
  275. package/src/migration-targets.ts +12 -0
  276. package/src/operator-access-stacks.test.ts +1 -1
  277. package/src/operator-access.test.ts +20 -4
  278. package/src/operator-access.ts +14 -0
  279. package/src/pool-budget-stack.ts +15 -0
  280. package/src/pool-budget-stacks.test.ts +39 -14
  281. package/src/public-role-isolation.test.ts +18 -8
  282. package/src/public-role-isolation.ts +31 -17
  283. package/src/release-contract.ts +3 -0
  284. package/src/release-manifest.test.ts +25 -0
  285. package/src/release-manifest.ts +59 -4
  286. package/src/remote-mcp-stack-config.test.ts +19 -9
  287. package/src/remote-mcp.test.ts +252 -57
  288. package/src/remote-mcp.ts +336 -77
  289. package/src/runtime-secrets.test.ts +37 -14
  290. package/src/runtime-secrets.ts +87 -27
  291. package/src/scheduler-setup.test.ts +201 -0
  292. package/src/scheduler-setup.ts +375 -0
  293. package/src/seal-key-rotation.test.ts +2 -2
  294. package/src/seal-key-rotation.ts +25 -25
  295. package/src/self-hosted-backup.test.ts +1 -0
  296. package/src/self-hosted-compose.test.ts +180 -6
  297. package/src/self-hosted-db-tls.test.ts +111 -0
  298. package/src/self-hosted-db-tls.ts +287 -0
  299. package/src/self-hosted-release-resolve.test.ts +71 -0
  300. package/src/self-hosted-release.ts +41 -11
  301. package/src/self-hosted-roles.integration.test.ts +2 -0
  302. package/src/self-hosted-roles.test.ts +87 -3
  303. package/src/self-hosted-roles.ts +101 -3
  304. package/src/self-hosted-scheduler-interval.test.ts +66 -0
  305. package/src/self-hosted-scheduler-interval.ts +79 -0
  306. package/src/self-hosted-start.test.ts +35 -2
  307. package/src/self-hosted-start.ts +20 -2
  308. package/src/self-hosted-upgrade.test.ts +104 -14
  309. package/src/self-hosted-upgrade.ts +123 -31
  310. package/src/stack-contract-parity.test.ts +79 -12
  311. package/src/stack-contract.test.ts +199 -31
  312. package/src/stack-contract.ts +472 -45
  313. package/src/stack-converge-secrets.test.ts +28 -13
  314. package/src/stack-converge.test.ts +203 -28
  315. package/src/stack-converge.ts +92 -30
  316. package/src/staging-database.test.ts +114 -0
  317. package/src/staging-database.ts +148 -0
  318. package/src/state-store.test.ts +2 -1
  319. package/src/upgrade/app-roll.ts +97 -19
  320. package/src/upgrade/converge.ts +249 -19
  321. package/src/upgrade/health.test.ts +63 -25
  322. package/src/upgrade/health.ts +54 -22
  323. package/src/upgrade/image-forms.test.ts +34 -0
  324. package/src/upgrade/image-forms.ts +23 -8
  325. package/src/upgrade/release-images.test.ts +5 -1
  326. package/src/upgrade/release-images.ts +22 -6
  327. package/src/upgrade/upgrade.test.ts +438 -46
  328. package/src/upgrade/upgrade.ts +64 -6
  329. package/src/upgrader-gcp.ts +66 -3
  330. package/src/upgrader-iam-preflight.test.ts +19 -0
  331. package/src/upgrader-iam-preflight.ts +6 -0
  332. package/src/upgrader-job.test.ts +101 -27
  333. package/src/upgrader-job.ts +27 -14
  334. package/src/upgrader-records.test.ts +97 -12
  335. package/src/upgrader-records.ts +28 -11
  336. package/src/upgrader-setup.test.ts +2 -0
  337. package/src/upgrader-setup.ts +10 -4
  338. package/src/wizard.ts +19 -0
  339. package/src/wizards/gcp-cost.test.ts +103 -0
  340. package/src/wizards/gcp-cost.ts +125 -18
  341. package/src/wizards/gcp.ts +164 -22
  342. package/stacks/aws/Pulumi.yaml +1 -1
  343. package/stacks/aws/index.ts +72 -14
  344. package/stacks/azure/index.ts +108 -21
  345. package/stacks/gcp/Pulumi.yaml +1 -1
  346. package/stacks/gcp/README.md +1 -0
  347. package/stacks/gcp/index.ts +133 -24
  348. package/stacks/gcp-firebase/Pulumi.yaml +1 -1
  349. package/stacks/gcp-firebase/index.ts +212 -34
  350. package/stacks/self-hosted/Pulumi.yaml +3 -0
  351. package/stacks/self-hosted/README.md +6 -1
  352. 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
- const GATEWAY = "caelo-production-gateway-bbb";
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 = { admin: `sha256:${"a".repeat(64)}`, gateway: `sha256:${"b".repeat(64)}` };
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
- "run.googleapis.com/network-interfaces": JSON.stringify([
65
- { network: "caelo-production-vpc", subnetwork: "caelo-production-subnet" },
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: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
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: "postgresql://admin_role@10.20.0.3:5432/cms_public?sslmode=require",
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: "postgresql://public_role@10.20.0.3:5432/cms_public?sslmode=require",
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 = indexOf(events, /^gcloud .*(add-iam-policy-binding|secrets create)/);
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
- // Nothing writes before the backup.
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
- expect(e.slice("gcloud ".length)).not.toMatch(ANY_WRITE);
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
- expect(gatewayRoll).toBeLessThan(deep);
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
- const writes = calls.filter((c) => ANY_WRITE.test(c));
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 = { admin: `${ADMIN}-00000-before`, gateway: `${GATEWAY}-00000-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: public_role holds no database roles (no database roles left on caelo-production-pg-abc)",
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: public_role's role memberships could not be revoked. Aborting upgrade.",
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 gateway's client file and timeout", async () => {
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
- expect(admin).toContain(`CAELO_MCP_PUBLIC_URL=https://${GATEWAY}-xyz.a.run.app`);
730
- expect(gateway).toContain(`CAELO_ADMIN_URL=https://${ADMIN}-xyz.a.run.app`);
731
- expect(gateway).toContain(`CAELO_MCP_PUBLIC_URL=https://${GATEWAY}-xyz.a.run.app`);
732
- expect(gateway).toContain(
733
- "--update-secrets=/secrets/operator-oauth-client/client.json=caelo-production-operator-oauth-client:latest",
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(gateway).toContain("--timeout=3600");
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(2);
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(2);
1037
+ expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(3);
753
1038
  });
754
1039
 
755
- it("#806 — on gcp, routes the discovery paths and adds the Cloud Armor rule before the roll", async () => {
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: [{ name: "public", pathRules: [{ paths: ["/api/*"], service: "gw" }] }],
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
- [/^compute security-policies describe/, ok(JSON.stringify({ rules: [] }))],
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
- expect(events).toContainEqual(
777
- expect.stringMatching(
778
- /^gcloud compute security-policies rules create 900 --security-policy=caelo-production-waf-1 /,
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
- expect(puts).toHaveLength(1);
782
- expect(puts[0]).toContain("/global/urlMaps/caelo-production-url-map-1");
783
- expect(puts[0]).toContain("/.well-known/oauth-authorization-server/*");
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(calls.filter((c) => CONVERGE_WRITE.test(c) || APP_ROLL_WRITE.test(c))).toEqual([]);
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(2);
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); the health check, the staging domain and remote MCP reach its run.app URL)`,
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(2);
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 () => {