@caelo-cms/provisioning 0.10.47 → 0.10.48

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 (315) hide show
  1. package/dist/agent/apply.d.ts +45 -0
  2. package/dist/agent/apply.d.ts.map +1 -0
  3. package/dist/agent/apply.js +83 -0
  4. package/dist/agent/apply.js.map +1 -0
  5. package/dist/agent/cli-provenance.d.ts +42 -0
  6. package/dist/agent/cli-provenance.d.ts.map +1 -0
  7. package/dist/agent/cli-provenance.js +138 -0
  8. package/dist/agent/cli-provenance.js.map +1 -0
  9. package/dist/agent/command.d.ts +79 -0
  10. package/dist/agent/command.d.ts.map +1 -0
  11. package/dist/agent/command.js +454 -0
  12. package/dist/agent/command.js.map +1 -0
  13. package/dist/agent/config.d.ts +30 -0
  14. package/dist/agent/config.d.ts.map +1 -0
  15. package/dist/agent/config.js +60 -0
  16. package/dist/agent/config.js.map +1 -0
  17. package/dist/agent/lock.d.ts +13 -0
  18. package/dist/agent/lock.d.ts.map +1 -0
  19. package/dist/agent/lock.js +100 -0
  20. package/dist/agent/lock.js.map +1 -0
  21. package/dist/agent/queue.d.ts +35 -0
  22. package/dist/agent/queue.d.ts.map +1 -0
  23. package/dist/agent/queue.js +50 -0
  24. package/dist/agent/queue.js.map +1 -0
  25. package/dist/agent/run.d.ts +97 -0
  26. package/dist/agent/run.d.ts.map +1 -0
  27. package/dist/agent/run.js +198 -0
  28. package/dist/agent/run.js.map +1 -0
  29. package/dist/agent/systemd.d.ts +55 -0
  30. package/dist/agent/systemd.d.ts.map +1 -0
  31. package/dist/agent/systemd.js +171 -0
  32. package/dist/agent/systemd.js.map +1 -0
  33. package/dist/agent/units.d.ts +44 -0
  34. package/dist/agent/units.d.ts.map +1 -0
  35. package/dist/agent/units.js +127 -0
  36. package/dist/agent/units.js.map +1 -0
  37. package/dist/agent-db.js +18362 -0
  38. package/dist/cli.js +119 -16
  39. package/dist/cli.js.map +1 -1
  40. package/dist/cloudsql-iam-user.d.ts +39 -0
  41. package/dist/cloudsql-iam-user.d.ts.map +1 -0
  42. package/dist/cloudsql-iam-user.js +119 -0
  43. package/dist/cloudsql-iam-user.js.map +1 -0
  44. package/dist/compose.d.ts +16 -0
  45. package/dist/compose.d.ts.map +1 -1
  46. package/dist/compose.js +26 -1
  47. package/dist/compose.js.map +1 -1
  48. package/dist/dns/index.d.ts +2 -0
  49. package/dist/dns/index.d.ts.map +1 -1
  50. package/dist/dns/index.js +3 -2
  51. package/dist/dns/index.js.map +1 -1
  52. package/dist/dns/manual.d.ts +5 -0
  53. package/dist/dns/manual.d.ts.map +1 -1
  54. package/dist/dns/manual.js +28 -12
  55. package/dist/dns/manual.js.map +1 -1
  56. package/dist/gcloud.d.ts +2 -0
  57. package/dist/gcloud.d.ts.map +1 -1
  58. package/dist/gcloud.js +1 -1
  59. package/dist/gcloud.js.map +1 -1
  60. package/dist/gcp-names.d.ts +15 -0
  61. package/dist/gcp-names.d.ts.map +1 -1
  62. package/dist/gcp-names.js +23 -0
  63. package/dist/gcp-names.js.map +1 -1
  64. package/dist/index.d.ts +6 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +4 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/infra-contract.d.ts +1 -1
  69. package/dist/infra-contract.js +7 -1
  70. package/dist/infra-contract.js.map +1 -1
  71. package/dist/install-job-runner.d.ts +13 -0
  72. package/dist/install-job-runner.d.ts.map +1 -0
  73. package/dist/install-job-runner.js +155 -0
  74. package/dist/install-job-runner.js.map +1 -0
  75. package/dist/install-job.d.ts +210 -0
  76. package/dist/install-job.d.ts.map +1 -0
  77. package/dist/install-job.js +449 -0
  78. package/dist/install-job.js.map +1 -0
  79. package/dist/install-state.d.ts +10 -1
  80. package/dist/install-state.d.ts.map +1 -1
  81. package/dist/install-state.js +10 -2
  82. package/dist/install-state.js.map +1 -1
  83. package/dist/lifecycle.d.ts.map +1 -1
  84. package/dist/lifecycle.js +19 -4
  85. package/dist/lifecycle.js.map +1 -1
  86. package/dist/migration-runner.d.ts +5 -0
  87. package/dist/migration-runner.d.ts.map +1 -1
  88. package/dist/migration-runner.js +1 -1
  89. package/dist/migration-runner.js.map +1 -1
  90. package/dist/operator-access.d.ts +14 -8
  91. package/dist/operator-access.d.ts.map +1 -1
  92. package/dist/operator-access.js +16 -102
  93. package/dist/operator-access.js.map +1 -1
  94. package/dist/run-mode.d.ts +27 -0
  95. package/dist/run-mode.d.ts.map +1 -0
  96. package/dist/run-mode.js +22 -0
  97. package/dist/run-mode.js.map +1 -0
  98. package/dist/runtime-secrets.d.ts +19 -0
  99. package/dist/runtime-secrets.d.ts.map +1 -1
  100. package/dist/runtime-secrets.js +44 -0
  101. package/dist/runtime-secrets.js.map +1 -1
  102. package/dist/self-hosted-roles.d.ts +22 -1
  103. package/dist/self-hosted-roles.d.ts.map +1 -1
  104. package/dist/self-hosted-roles.js +81 -12
  105. package/dist/self-hosted-roles.js.map +1 -1
  106. package/dist/self-hosted-upgrade.d.ts +17 -0
  107. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  108. package/dist/self-hosted-upgrade.js +16 -1
  109. package/dist/self-hosted-upgrade.js.map +1 -1
  110. package/dist/stack-contract.d.ts +6 -0
  111. package/dist/stack-contract.d.ts.map +1 -1
  112. package/dist/stack-contract.js +10 -1
  113. package/dist/stack-contract.js.map +1 -1
  114. package/dist/upgrade/converge.d.ts +6 -0
  115. package/dist/upgrade/converge.d.ts.map +1 -1
  116. package/dist/upgrade/converge.js +59 -2
  117. package/dist/upgrade/converge.js.map +1 -1
  118. package/dist/upgrade/gcp-lookup.d.ts +6 -0
  119. package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
  120. package/dist/upgrade/gcp-lookup.js +25 -0
  121. package/dist/upgrade/gcp-lookup.js.map +1 -1
  122. package/dist/upgrade/release-images.d.ts +12 -0
  123. package/dist/upgrade/release-images.d.ts.map +1 -1
  124. package/dist/upgrade/release-images.js +31 -0
  125. package/dist/upgrade/release-images.js.map +1 -1
  126. package/dist/upgrade/upgrade.d.ts +5 -0
  127. package/dist/upgrade/upgrade.d.ts.map +1 -1
  128. package/dist/upgrade/upgrade.js +28 -1
  129. package/dist/upgrade/upgrade.js.map +1 -1
  130. package/dist/upgrader-assess.d.ts +82 -0
  131. package/dist/upgrader-assess.d.ts.map +1 -0
  132. package/dist/upgrader-assess.js +116 -0
  133. package/dist/upgrader-assess.js.map +1 -0
  134. package/dist/upgrader-gcp.d.ts +17 -0
  135. package/dist/upgrader-gcp.d.ts.map +1 -0
  136. package/dist/upgrader-gcp.js +87 -0
  137. package/dist/upgrader-gcp.js.map +1 -0
  138. package/dist/upgrader-job.d.ts +131 -0
  139. package/dist/upgrader-job.d.ts.map +1 -0
  140. package/dist/upgrader-job.js +259 -0
  141. package/dist/upgrader-job.js.map +1 -0
  142. package/dist/upgrader-reporter.d.ts +35 -0
  143. package/dist/upgrader-reporter.d.ts.map +1 -0
  144. package/dist/upgrader-reporter.js +79 -0
  145. package/dist/upgrader-reporter.js.map +1 -0
  146. package/dist/upgrader-setup.d.ts +59 -0
  147. package/dist/upgrader-setup.d.ts.map +1 -0
  148. package/dist/upgrader-setup.js +308 -0
  149. package/dist/upgrader-setup.js.map +1 -0
  150. package/dist/wizard.d.ts +6 -0
  151. package/dist/wizard.d.ts.map +1 -1
  152. package/dist/wizard.js +32 -4
  153. package/dist/wizard.js.map +1 -1
  154. package/dist/wizards/gcp-firebase.d.ts +3 -0
  155. package/dist/wizards/gcp-firebase.d.ts.map +1 -1
  156. package/dist/wizards/gcp-firebase.js +1 -0
  157. package/dist/wizards/gcp-firebase.js.map +1 -1
  158. package/dist/wizards/gcp-install-job.d.ts +74 -0
  159. package/dist/wizards/gcp-install-job.d.ts.map +1 -0
  160. package/dist/wizards/gcp-install-job.js +398 -0
  161. package/dist/wizards/gcp-install-job.js.map +1 -0
  162. package/dist/wizards/gcp-prompts.d.ts +3 -0
  163. package/dist/wizards/gcp-prompts.d.ts.map +1 -0
  164. package/dist/wizards/gcp-prompts.js +25 -0
  165. package/dist/wizards/gcp-prompts.js.map +1 -0
  166. package/dist/wizards/gcp-pulumi.d.ts +4 -2
  167. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  168. package/dist/wizards/gcp-pulumi.js +1 -1
  169. package/dist/wizards/gcp-pulumi.js.map +1 -1
  170. package/dist/wizards/gcp.d.ts +40 -0
  171. package/dist/wizards/gcp.d.ts.map +1 -1
  172. package/dist/wizards/gcp.js +223 -59
  173. package/dist/wizards/gcp.js.map +1 -1
  174. package/package.json +6 -4
  175. package/src/adapter.ts +97 -0
  176. package/src/adc-impersonation.ts +176 -0
  177. package/src/admin-domain.test.ts +254 -0
  178. package/src/admin-domain.ts +245 -0
  179. package/src/admin-memory.test.ts +151 -0
  180. package/src/agent/apply.test.ts +185 -0
  181. package/src/agent/apply.ts +124 -0
  182. package/src/agent/cli-provenance.test.ts +159 -0
  183. package/src/agent/cli-provenance.ts +191 -0
  184. package/src/agent/command.test.ts +77 -0
  185. package/src/agent/command.ts +564 -0
  186. package/src/agent/config.ts +86 -0
  187. package/src/agent/lock-config.test.ts +97 -0
  188. package/src/agent/lock.ts +110 -0
  189. package/src/agent/queue.test.ts +10 -0
  190. package/src/agent/queue.ts +91 -0
  191. package/src/agent/run.test.ts +382 -0
  192. package/src/agent/run.ts +295 -0
  193. package/src/agent/systemd.ts +209 -0
  194. package/src/agent/units.test.ts +223 -0
  195. package/src/agent/units.ts +163 -0
  196. package/src/azure-front-door.test.ts +119 -0
  197. package/src/azure-front-door.ts +56 -0
  198. package/src/bootstrap-token.ts +20 -0
  199. package/src/caddy.ts +112 -0
  200. package/src/cdn-copy.ts +91 -0
  201. package/src/cert-wait.test.ts +194 -0
  202. package/src/cert-wait.ts +163 -0
  203. package/src/checkpoint-secrets.test.ts +380 -0
  204. package/src/checkpoint-secrets.ts +283 -0
  205. package/src/cli-routing.test.ts +45 -0
  206. package/src/cli-routing.ts +43 -0
  207. package/src/cli.ts +1491 -0
  208. package/src/cloudsql-iam-user.ts +147 -0
  209. package/src/compose.ts +334 -0
  210. package/src/default-firewall.test.ts +411 -0
  211. package/src/default-firewall.ts +342 -0
  212. package/src/deployed-release.test.ts +231 -0
  213. package/src/deployed-release.ts +220 -0
  214. package/src/dns/cloudflare.ts +182 -0
  215. package/src/dns/groups.test.ts +76 -0
  216. package/src/dns/groups.ts +86 -0
  217. package/src/dns/index.ts +56 -0
  218. package/src/dns/manual.ts +130 -0
  219. package/src/dns/types.ts +27 -0
  220. package/src/gateway-credentials.test.ts +180 -0
  221. package/src/gateway-credentials.ts +144 -0
  222. package/src/gcloud-components.test.ts +76 -0
  223. package/src/gcloud-components.ts +85 -0
  224. package/src/gcloud-retry.ts +41 -0
  225. package/src/gcloud-runner.test.ts +61 -0
  226. package/src/gcloud.test.ts +99 -0
  227. package/src/gcloud.ts +322 -0
  228. package/src/gcp-names.ts +135 -0
  229. package/src/index.test.ts +332 -0
  230. package/src/index.ts +114 -0
  231. package/src/infra-contract.ts +32 -0
  232. package/src/install-job-runner.ts +166 -0
  233. package/src/install-job.test.ts +566 -0
  234. package/src/install-job.ts +542 -0
  235. package/src/install-state.test.ts +239 -0
  236. package/src/install-state.ts +583 -0
  237. package/src/internal-token.ts +35 -0
  238. package/src/lifecycle.ts +900 -0
  239. package/src/mcp-iap-stacks.test.ts +58 -0
  240. package/src/mcp-iap.test.ts +149 -0
  241. package/src/mcp-iap.ts +131 -0
  242. package/src/media-volume.test.ts +296 -0
  243. package/src/migration-runner.ts +365 -0
  244. package/src/operator-access-stacks.test.ts +55 -0
  245. package/src/operator-access.test.ts +332 -0
  246. package/src/operator-access.ts +554 -0
  247. package/src/ops-backend.test.ts +320 -0
  248. package/src/ops-backend.ts +414 -0
  249. package/src/provisioner-identity.test.ts +482 -0
  250. package/src/provisioner-identity.ts +407 -0
  251. package/src/redirects-emit.ts +175 -0
  252. package/src/region-data.ts +428 -0
  253. package/src/regions.test.ts +277 -0
  254. package/src/regions.ts +370 -0
  255. package/src/release-contract.ts +57 -0
  256. package/src/release-manifest.test.ts +293 -0
  257. package/src/release-manifest.ts +430 -0
  258. package/src/run-mode.ts +40 -0
  259. package/src/runtime-secrets.test.ts +627 -0
  260. package/src/runtime-secrets.ts +754 -0
  261. package/src/self-command.test.ts +14 -0
  262. package/src/self-command.ts +21 -0
  263. package/src/self-hosted-compose.test.ts +177 -0
  264. package/src/self-hosted-release.ts +240 -0
  265. package/src/self-hosted-roles.integration.test.ts +161 -0
  266. package/src/self-hosted-roles.test.ts +124 -0
  267. package/src/self-hosted-roles.ts +203 -0
  268. package/src/self-hosted-upgrade.test.ts +394 -0
  269. package/src/self-hosted-upgrade.ts +548 -0
  270. package/src/stack-contract-parity.test.ts +199 -0
  271. package/src/stack-contract.test.ts +391 -0
  272. package/src/stack-contract.ts +888 -0
  273. package/src/stack-converge-secrets.test.ts +421 -0
  274. package/src/stack-converge.test.ts +833 -0
  275. package/src/stack-converge.ts +909 -0
  276. package/src/staging-admin.test.ts +246 -0
  277. package/src/staging-admin.ts +324 -0
  278. package/src/state-attach.test.ts +211 -0
  279. package/src/state-attach.ts +132 -0
  280. package/src/state-backend.test.ts +145 -0
  281. package/src/state-backend.ts +262 -0
  282. package/src/state-command.ts +293 -0
  283. package/src/state-migrate.test.ts +479 -0
  284. package/src/state-migrate.ts +434 -0
  285. package/src/state-pulumi.ts +289 -0
  286. package/src/state-store.test.ts +212 -0
  287. package/src/state-store.ts +243 -0
  288. package/src/upgrade/app-roll.ts +486 -0
  289. package/src/upgrade/backup.ts +109 -0
  290. package/src/upgrade/converge.ts +437 -0
  291. package/src/upgrade/gcp-lookup.ts +167 -0
  292. package/src/upgrade/health.test.ts +279 -0
  293. package/src/upgrade/health.ts +305 -0
  294. package/src/upgrade/release-images.test.ts +119 -0
  295. package/src/upgrade/release-images.ts +391 -0
  296. package/src/upgrade/reporter.ts +46 -0
  297. package/src/upgrade/upgrade.test.ts +626 -0
  298. package/src/upgrade/upgrade.ts +249 -0
  299. package/src/upgrader-assess.test.ts +265 -0
  300. package/src/upgrader-assess.ts +233 -0
  301. package/src/upgrader-gcp.ts +123 -0
  302. package/src/upgrader-job.test.ts +408 -0
  303. package/src/upgrader-job.ts +499 -0
  304. package/src/upgrader-reporter.test.ts +46 -0
  305. package/src/upgrader-reporter.ts +105 -0
  306. package/src/upgrader-setup.test.ts +169 -0
  307. package/src/upgrader-setup.ts +355 -0
  308. package/src/wizard.ts +425 -0
  309. package/src/wizards/gcp-cost.ts +188 -0
  310. package/src/wizards/gcp-firebase.ts +72 -0
  311. package/src/wizards/gcp-install-job.ts +506 -0
  312. package/src/wizards/gcp-prompts.ts +26 -0
  313. package/src/wizards/gcp-pulumi.test.ts +246 -0
  314. package/src/wizards/gcp-pulumi.ts +516 -0
  315. package/src/wizards/gcp.ts +1935 -0
@@ -0,0 +1,365 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * P21 ship 3 — shared migration runner. Used by both the GCP wizard
5
+ * (apps/admin/scripts/wizards/gcp.ts) AND the lifecycle `upgrade`
6
+ * command (lifecycle.ts).
7
+ *
8
+ * Spawns a one-shot Cloud Run Job that invokes
9
+ * `bun /app/packages/migrations/src/migrate.ts <target>` against the
10
+ * private-IP Cloud SQL instance. The job inherits the running admin
11
+ * Cloud Run's image (so it always carries the matching migration set)
12
+ * + its database host + its network/subnet. Its database env follows the
13
+ * env contract (stack-contract.ts): password-less URLs plus the password
14
+ * as a Secret Manager reference, so the job spec never holds a password.
15
+ *
16
+ * Idempotent: drizzle's `__drizzle_migrations` table tracks applied
17
+ * versions, so re-runs only apply NEW migrations. Returns `{ok}` so
18
+ * callers (especially `upgrade`) can abort cleanly when a migration
19
+ * fails BEFORE traffic shifts to the new revision.
20
+ */
21
+
22
+ import { spinner } from "@clack/prompts";
23
+ import { green, red } from "kleur/colors";
24
+ import { gcloud as defaultGcloud } from "./gcloud.js";
25
+ import type { GcloudRunner } from "./gcloud-retry.js";
26
+ import { GCP_STACK_ENV, gcpSecretId, runServiceAccountEmail } from "./gcp-names.js";
27
+ import { databaseUrls, SERVICE_SECRET_ENV } from "./stack-contract.js";
28
+ import { liveContainerEnv, liveDatabaseHost } from "./stack-converge.js";
29
+
30
+ interface MigrationRunnerOpts {
31
+ readonly projectId: string;
32
+ readonly region: string;
33
+ }
34
+
35
+ interface AdminConfig {
36
+ readonly imageRef: string;
37
+ /** Cloud SQL private IP, from the admin's ADMIN_DATABASE_URL. */
38
+ readonly databaseHost: string;
39
+ readonly networkRef: string;
40
+ readonly subnetRef: string;
41
+ /**
42
+ * Whether the admin already reads the database password from Secret
43
+ * Manager. If not, its image may predate `databaseUrlFromEnv` and can't
44
+ * run a job with password-less URLs.
45
+ */
46
+ readonly secretEnv: boolean;
47
+ }
48
+
49
+ /** Why a job on the installed admin image would fail, for an install `upgrade` hasn't moved over. */
50
+ const NEEDS_UPGRADE =
51
+ "the admin still reads the database password from its plain env, so its image may predate Secret Manager runtime secrets and can't run a job with password-less URLs. Run `upgrade` first.";
52
+
53
+ /**
54
+ * The database env of a migration/truncate job: the admin's URLs (no
55
+ * password) as plain vars, the password vars as Secret Manager references
56
+ * — the same pairs the admin service carries. The job runs as the admin's
57
+ * run SA, which can read them.
58
+ */
59
+ export function migrationJobEnvArgs(databaseHost: string, env = GCP_STACK_ENV): string[] {
60
+ const urls = databaseUrls(databaseHost);
61
+ const passwords = (["ADMIN_DATABASE_PASSWORD", "PUBLIC_ADMIN_DATABASE_PASSWORD"] as const).map(
62
+ (name) => {
63
+ const secret = SERVICE_SECRET_ENV.admin[name];
64
+ if (!secret) throw new Error(`the admin env contract has no ${name}`);
65
+ return `${name}=${gcpSecretId(env, secret)}:latest`;
66
+ },
67
+ );
68
+ return [
69
+ `--set-env-vars=ADMIN_DATABASE_URL=${urls.admin},PUBLIC_ADMIN_DATABASE_URL=${urls.publicAdmin}`,
70
+ `--set-secrets=${passwords.join(",")}`,
71
+ ];
72
+ }
73
+
74
+ /**
75
+ * Resolve the running admin Cloud Run service's image + database host +
76
+ * VPC config. Returns null on any failure; caller surfaces the error.
77
+ */
78
+ export async function readAdminConfig(
79
+ opts: MigrationRunnerOpts,
80
+ gcloud: GcloudRunner,
81
+ ): Promise<AdminConfig | null> {
82
+ // Find the actual deployed service name (Pulumi appends a 7-char
83
+ // suffix; hardcoding the bare name 404s).
84
+ const list = await gcloud([
85
+ "run",
86
+ "services",
87
+ "list",
88
+ "--region",
89
+ opts.region,
90
+ "--project",
91
+ opts.projectId,
92
+ "--filter",
93
+ "metadata.name~^caelo-production-admin",
94
+ "--format=value(metadata.name)",
95
+ ]);
96
+ if (!list.ok) return null;
97
+ const serviceName = list.stdout.trim().split("\n")[0]?.trim();
98
+ if (!serviceName) return null;
99
+
100
+ const descr = await gcloud([
101
+ "run",
102
+ "services",
103
+ "describe",
104
+ serviceName,
105
+ "--region",
106
+ opts.region,
107
+ "--project",
108
+ opts.projectId,
109
+ "--format=json",
110
+ ]);
111
+ if (!descr.ok) return null;
112
+ return parseAdminConfig(descr.stdout);
113
+ }
114
+
115
+ /**
116
+ * The job config from the admin service (`gcloud run services describe
117
+ * --format=json`), or null when something the job needs is missing.
118
+ */
119
+ export function parseAdminConfig(serviceJson: string): AdminConfig | null {
120
+ try {
121
+ const d = JSON.parse(serviceJson) as {
122
+ spec: {
123
+ template: {
124
+ metadata?: { annotations?: Record<string, string> };
125
+ spec: {
126
+ containers: { image?: string }[];
127
+ vpcAccess?: { networkInterfaces?: { network?: string; subnetwork?: string }[] };
128
+ };
129
+ };
130
+ };
131
+ };
132
+ const c = d.spec.template.spec.containers[0];
133
+ const imageRef = c?.image ?? "";
134
+ const liveEnv = liveContainerEnv(serviceJson);
135
+ const host = liveDatabaseHost(liveEnv);
136
+ let networkRef = "";
137
+ let subnetRef = "";
138
+ const niAnnotation =
139
+ d.spec.template.metadata?.annotations?.["run.googleapis.com/network-interfaces"];
140
+ if (niAnnotation) {
141
+ try {
142
+ const parsed = JSON.parse(niAnnotation) as { network?: string; subnetwork?: string }[];
143
+ networkRef = parsed[0]?.network ?? "";
144
+ subnetRef = parsed[0]?.subnetwork ?? "";
145
+ } catch {
146
+ // fall through
147
+ }
148
+ }
149
+ if (!networkRef || !subnetRef) {
150
+ const ni = d.spec.template.spec.vpcAccess?.networkInterfaces?.[0];
151
+ networkRef ||= ni?.network ?? "";
152
+ subnetRef ||= ni?.subnetwork ?? "";
153
+ }
154
+ if (!imageRef || !host.ok || !networkRef || !subnetRef) return null;
155
+ return {
156
+ imageRef,
157
+ databaseHost: host.host,
158
+ networkRef,
159
+ subnetRef,
160
+ secretEnv: liveEnv.get("ADMIN_DATABASE_PASSWORD")?.kind === "secret",
161
+ };
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Run admin + public migrations against Cloud SQL. Returns `{ok}` so
169
+ * callers can decide what to do on failure (the wizard exits the
170
+ * process; upgrade aborts before rolling Cloud Run).
171
+ */
172
+ /**
173
+ * v0.4.0 — Run TRUNCATE against cms_admin + cms_public via the same
174
+ * one-shot Cloud Run Job pattern the migrator uses. Wipes content /
175
+ * history / chat tables; keeps users / roles / providers / domains /
176
+ * site_defaults / provisioning_outputs / site_ai_memory.
177
+ *
178
+ * Useful after a schema change (e.g. v0.4.0's module/content split)
179
+ * when the operator wants the AI to start fresh against the new shape
180
+ * without destroying the install.
181
+ */
182
+ export async function truncateViaCloudRunJob(
183
+ opts: MigrationRunnerOpts,
184
+ ): Promise<{ ok: boolean; error?: string }> {
185
+ const gcloud = defaultGcloud;
186
+ const sAdmin = spinner();
187
+ sAdmin.start("Reading admin Cloud Run config for truncate job...");
188
+ const cfg = await readAdminConfig(opts, gcloud);
189
+ if (!cfg) {
190
+ sAdmin.stop(red("Couldn't resolve admin config — admin may not be deployed yet"));
191
+ return { ok: false, error: "admin-config-unresolved" };
192
+ }
193
+ if (!cfg.secretEnv) {
194
+ sAdmin.stop(red(`Not truncating: ${NEEDS_UPGRADE}`));
195
+ return { ok: false, error: NEEDS_UPGRADE };
196
+ }
197
+ sAdmin.stop(green(`Admin config resolved (${cfg.imageRef.slice(-19)})`));
198
+
199
+ for (const target of ["admin", "public"] as const) {
200
+ const s = spinner();
201
+ s.start(`TRUNCATEing ${target} tables via one-shot Cloud Run Job...`);
202
+ await gcloud([
203
+ "run",
204
+ "jobs",
205
+ "delete",
206
+ `caelo-truncate-${target}`,
207
+ "--region",
208
+ opts.region,
209
+ "--project",
210
+ opts.projectId,
211
+ "--quiet",
212
+ ]);
213
+ const create = await gcloud([
214
+ "run",
215
+ "jobs",
216
+ "create",
217
+ `caelo-truncate-${target}`,
218
+ `--image=${cfg.imageRef}`,
219
+ "--region",
220
+ opts.region,
221
+ "--project",
222
+ opts.projectId,
223
+ "--service-account",
224
+ runServiceAccountEmail(opts.projectId, GCP_STACK_ENV),
225
+ "--network",
226
+ cfg.networkRef.split("/").pop() ?? cfg.networkRef,
227
+ "--subnet",
228
+ cfg.subnetRef.split("/").pop() ?? cfg.subnetRef,
229
+ "--vpc-egress=private-ranges-only",
230
+ "--command=bun",
231
+ `--args=--bun,/app/packages/migrations/src/truncate.ts,${target}`,
232
+ ...migrationJobEnvArgs(cfg.databaseHost),
233
+ "--max-retries=0",
234
+ "--task-timeout=5m",
235
+ "--quiet",
236
+ ]);
237
+ if (!create.ok) {
238
+ s.stop(red(`Job create failed: ${create.stderr.trim()}`));
239
+ return { ok: false, error: `truncate-${target}-create: ${create.stderr.trim()}` };
240
+ }
241
+ const exec = await gcloud([
242
+ "run",
243
+ "jobs",
244
+ "execute",
245
+ `caelo-truncate-${target}`,
246
+ "--region",
247
+ opts.region,
248
+ "--project",
249
+ opts.projectId,
250
+ "--wait",
251
+ ]);
252
+ if (!exec.ok) {
253
+ s.stop(red(`${target} truncate failed: ${exec.stderr.trim()}`));
254
+ return { ok: false, error: `truncate-${target}-exec: ${exec.stderr.trim()}` };
255
+ }
256
+ s.stop(green(`${target} truncated`));
257
+ }
258
+ return { ok: true };
259
+ }
260
+
261
+ export async function runMigrationsViaCloudRunJob(
262
+ opts: MigrationRunnerOpts & {
263
+ /**
264
+ * v0.9.2 — explicit image to run migrations against. When omitted,
265
+ * falls back to the admin Cloud Run's CURRENT image (pre-upgrade
266
+ * shape). The upgrade lifecycle MUST pass the NEW image being
267
+ * rolled to — otherwise migrations run with the old image, which
268
+ * doesn't carry the new migration files, so the bookkeeping
269
+ * silently reports "no new migrations to apply" and the new admin
270
+ * image then hits "column does not exist" on every query for new
271
+ * schema. Operator-side recovery: `gcloud run jobs update
272
+ * caelo-migrate-admin --image=<new-version>` then execute.
273
+ */
274
+ readonly imageOverride?: string;
275
+ },
276
+ deps: { readonly run?: GcloudRunner } = {},
277
+ ): Promise<{ ok: boolean; error?: string }> {
278
+ const gcloud = deps.run ?? defaultGcloud;
279
+ const sAdmin = spinner();
280
+ sAdmin.start("Reading admin Cloud Run config for migration job...");
281
+ const cfg = await readAdminConfig(opts, gcloud);
282
+ if (!cfg) {
283
+ sAdmin.stop(red("Couldn't resolve admin config — admin may not be deployed yet"));
284
+ return { ok: false, error: "admin-config-unresolved" };
285
+ }
286
+ // With an override (upgrade) the job runs the NEW release, which reads
287
+ // the split env; without one it runs the installed image.
288
+ if (!opts.imageOverride && !cfg.secretEnv) {
289
+ sAdmin.stop(red(`Not migrating: ${NEEDS_UPGRADE}`));
290
+ return { ok: false, error: NEEDS_UPGRADE };
291
+ }
292
+ const migrationImage = opts.imageOverride ?? cfg.imageRef;
293
+ if (opts.imageOverride && opts.imageOverride !== cfg.imageRef) {
294
+ sAdmin.stop(
295
+ green(
296
+ `Admin config resolved; migrating against NEW image (${migrationImage.slice(-19)}) instead of current (${cfg.imageRef.slice(-19)})`,
297
+ ),
298
+ );
299
+ } else {
300
+ sAdmin.stop(green(`Admin config resolved (${migrationImage.slice(-19)})`));
301
+ }
302
+
303
+ for (const target of ["admin", "public"] as const) {
304
+ const s = spinner();
305
+ s.start(`Applying ${target} migrations via one-shot Cloud Run Job...`);
306
+ // Delete-then-create so each invocation runs with the freshest
307
+ // image + env (no stale job specs).
308
+ await gcloud([
309
+ "run",
310
+ "jobs",
311
+ "delete",
312
+ `caelo-migrate-${target}`,
313
+ "--region",
314
+ opts.region,
315
+ "--project",
316
+ opts.projectId,
317
+ "--quiet",
318
+ ]);
319
+ const create = await gcloud([
320
+ "run",
321
+ "jobs",
322
+ "create",
323
+ `caelo-migrate-${target}`,
324
+ `--image=${migrationImage}`,
325
+ "--region",
326
+ opts.region,
327
+ "--project",
328
+ opts.projectId,
329
+ "--service-account",
330
+ runServiceAccountEmail(opts.projectId, GCP_STACK_ENV),
331
+ "--network",
332
+ cfg.networkRef.split("/").pop() ?? cfg.networkRef,
333
+ "--subnet",
334
+ cfg.subnetRef.split("/").pop() ?? cfg.subnetRef,
335
+ "--vpc-egress=private-ranges-only",
336
+ "--command=bun",
337
+ `--args=--bun,/app/packages/migrations/src/migrate.ts,${target}`,
338
+ ...migrationJobEnvArgs(cfg.databaseHost),
339
+ "--max-retries=0",
340
+ "--task-timeout=10m",
341
+ "--quiet",
342
+ ]);
343
+ if (!create.ok) {
344
+ s.stop(red(`Job create failed: ${create.stderr.trim()}`));
345
+ return { ok: false, error: `migrate-${target}-create: ${create.stderr.trim()}` };
346
+ }
347
+ const exec = await gcloud([
348
+ "run",
349
+ "jobs",
350
+ "execute",
351
+ `caelo-migrate-${target}`,
352
+ "--region",
353
+ opts.region,
354
+ "--project",
355
+ opts.projectId,
356
+ "--wait",
357
+ ]);
358
+ if (!exec.ok) {
359
+ s.stop(red(`${target} migrations failed: ${exec.stderr.trim()}`));
360
+ return { ok: false, error: `migrate-${target}-exec: ${exec.stderr.trim()}` };
361
+ }
362
+ s.stop(green(`${target} migrations applied`));
363
+ }
364
+ return { ok: true };
365
+ }
@@ -0,0 +1,55 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * The stacks must give the admin's run SA no IAM-policy rights for operator
5
+ * access: the sync job (operator-access.ts, CLI-owned) is the only principal
6
+ * allowed to change who passes IAP. What the stacks DO carry for it: the
7
+ * Cloud SQL flag that lets the job log in as its service account, and the
8
+ * admin env var naming the job (stack-contract.ts). Pure-string checks, like
9
+ * mcp-iap-stacks.test.ts.
10
+ */
11
+
12
+ import { describe, expect, it } from "bun:test";
13
+ import { readFileSync } from "node:fs";
14
+ import { resolve } from "node:path";
15
+ import { adminEnvContract, databaseUrls } from "./stack-contract.js";
16
+
17
+ const stack = (provider: string) =>
18
+ readFileSync(resolve(import.meta.dir, `../stacks/${provider}/index.ts`), "utf8");
19
+
20
+ describe.each(["gcp", "gcp-firebase"])("%s stack — operator access", (provider) => {
21
+ const src = stack(provider);
22
+
23
+ it("grants no IAM-policy-writing role to anyone for operator access", () => {
24
+ expect(src).not.toContain("caeloOperatorAccess");
25
+ expect(src).not.toContain("caeloAdminIapLookup");
26
+ expect(src).not.toContain("IAMCustomRole");
27
+ expect(src).not.toMatch(/setIamPolicy|roles\/iap\.admin|roles\/iam\.serviceAccountAdmin/);
28
+ });
29
+
30
+ it("turns on Cloud SQL IAM database authentication for the job's read-only user", () => {
31
+ expect(src).toContain('{ name: "cloudsql.iam_authentication", value: "on" }');
32
+ });
33
+
34
+ it("deploys the admin with the env contract, which names the sync job", () => {
35
+ expect(src).toContain("adminEnvContract(");
36
+ });
37
+ });
38
+
39
+ describe("admin env contract — operator access", () => {
40
+ it("names the sync job the admin may start", () => {
41
+ const env = adminEnvContract({
42
+ provider: "gcp",
43
+ projectId: "acme",
44
+ env: "production",
45
+ domain: "acme.com",
46
+ stagingHost: "staging.acme.com",
47
+ region: "europe-west1",
48
+ databaseUrls: databaseUrls("10.0.0.3"),
49
+ });
50
+ expect(env).toContainEqual({
51
+ name: "CAELO_OPERATOR_ACCESS_JOB",
52
+ value: "projects/acme/locations/europe-west1/jobs/caelo-production-operator-access-sync",
53
+ });
54
+ });
55
+ });