@caelo-cms/provisioning 0.10.46 → 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 (342) hide show
  1. package/dist/adc-impersonation.d.ts.map +1 -1
  2. package/dist/adc-impersonation.js +23 -1
  3. package/dist/adc-impersonation.js.map +1 -1
  4. package/dist/agent/apply.d.ts +45 -0
  5. package/dist/agent/apply.d.ts.map +1 -0
  6. package/dist/agent/apply.js +83 -0
  7. package/dist/agent/apply.js.map +1 -0
  8. package/dist/agent/cli-provenance.d.ts +42 -0
  9. package/dist/agent/cli-provenance.d.ts.map +1 -0
  10. package/dist/agent/cli-provenance.js +138 -0
  11. package/dist/agent/cli-provenance.js.map +1 -0
  12. package/dist/agent/command.d.ts +79 -0
  13. package/dist/agent/command.d.ts.map +1 -0
  14. package/dist/agent/command.js +454 -0
  15. package/dist/agent/command.js.map +1 -0
  16. package/dist/agent/config.d.ts +30 -0
  17. package/dist/agent/config.d.ts.map +1 -0
  18. package/dist/agent/config.js +60 -0
  19. package/dist/agent/config.js.map +1 -0
  20. package/dist/agent/lock.d.ts +13 -0
  21. package/dist/agent/lock.d.ts.map +1 -0
  22. package/dist/agent/lock.js +100 -0
  23. package/dist/agent/lock.js.map +1 -0
  24. package/dist/agent/queue.d.ts +35 -0
  25. package/dist/agent/queue.d.ts.map +1 -0
  26. package/dist/agent/queue.js +50 -0
  27. package/dist/agent/queue.js.map +1 -0
  28. package/dist/agent/run.d.ts +97 -0
  29. package/dist/agent/run.d.ts.map +1 -0
  30. package/dist/agent/run.js +198 -0
  31. package/dist/agent/run.js.map +1 -0
  32. package/dist/agent/systemd.d.ts +55 -0
  33. package/dist/agent/systemd.d.ts.map +1 -0
  34. package/dist/agent/systemd.js +171 -0
  35. package/dist/agent/systemd.js.map +1 -0
  36. package/dist/agent/units.d.ts +44 -0
  37. package/dist/agent/units.d.ts.map +1 -0
  38. package/dist/agent/units.js +127 -0
  39. package/dist/agent/units.js.map +1 -0
  40. package/dist/agent-db.js +18362 -0
  41. package/dist/cli.js +122 -16
  42. package/dist/cli.js.map +1 -1
  43. package/dist/cloudsql-iam-user.d.ts +39 -0
  44. package/dist/cloudsql-iam-user.d.ts.map +1 -0
  45. package/dist/cloudsql-iam-user.js +119 -0
  46. package/dist/cloudsql-iam-user.js.map +1 -0
  47. package/dist/compose.d.ts +16 -0
  48. package/dist/compose.d.ts.map +1 -1
  49. package/dist/compose.js +26 -1
  50. package/dist/compose.js.map +1 -1
  51. package/dist/dns/index.d.ts +2 -0
  52. package/dist/dns/index.d.ts.map +1 -1
  53. package/dist/dns/index.js +3 -2
  54. package/dist/dns/index.js.map +1 -1
  55. package/dist/dns/manual.d.ts +5 -0
  56. package/dist/dns/manual.d.ts.map +1 -1
  57. package/dist/dns/manual.js +28 -12
  58. package/dist/dns/manual.js.map +1 -1
  59. package/dist/gcloud.d.ts +2 -0
  60. package/dist/gcloud.d.ts.map +1 -1
  61. package/dist/gcloud.js +1 -1
  62. package/dist/gcloud.js.map +1 -1
  63. package/dist/gcp-names.d.ts +15 -0
  64. package/dist/gcp-names.d.ts.map +1 -1
  65. package/dist/gcp-names.js +23 -0
  66. package/dist/gcp-names.js.map +1 -1
  67. package/dist/index.d.ts +6 -0
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +4 -0
  70. package/dist/index.js.map +1 -1
  71. package/dist/infra-contract.d.ts +1 -1
  72. package/dist/infra-contract.js +7 -1
  73. package/dist/infra-contract.js.map +1 -1
  74. package/dist/install-job-runner.d.ts +13 -0
  75. package/dist/install-job-runner.d.ts.map +1 -0
  76. package/dist/install-job-runner.js +155 -0
  77. package/dist/install-job-runner.js.map +1 -0
  78. package/dist/install-job.d.ts +210 -0
  79. package/dist/install-job.d.ts.map +1 -0
  80. package/dist/install-job.js +449 -0
  81. package/dist/install-job.js.map +1 -0
  82. package/dist/install-state.d.ts +19 -1
  83. package/dist/install-state.d.ts.map +1 -1
  84. package/dist/install-state.js +31 -3
  85. package/dist/install-state.js.map +1 -1
  86. package/dist/lifecycle.d.ts.map +1 -1
  87. package/dist/lifecycle.js +24 -5
  88. package/dist/lifecycle.js.map +1 -1
  89. package/dist/migration-runner.d.ts +5 -0
  90. package/dist/migration-runner.d.ts.map +1 -1
  91. package/dist/migration-runner.js +1 -1
  92. package/dist/migration-runner.js.map +1 -1
  93. package/dist/operator-access.d.ts +14 -8
  94. package/dist/operator-access.d.ts.map +1 -1
  95. package/dist/operator-access.js +16 -102
  96. package/dist/operator-access.js.map +1 -1
  97. package/dist/run-mode.d.ts +27 -0
  98. package/dist/run-mode.d.ts.map +1 -0
  99. package/dist/run-mode.js +22 -0
  100. package/dist/run-mode.js.map +1 -0
  101. package/dist/runtime-secrets.d.ts +19 -0
  102. package/dist/runtime-secrets.d.ts.map +1 -1
  103. package/dist/runtime-secrets.js +44 -0
  104. package/dist/runtime-secrets.js.map +1 -1
  105. package/dist/self-hosted-roles.d.ts +22 -1
  106. package/dist/self-hosted-roles.d.ts.map +1 -1
  107. package/dist/self-hosted-roles.js +81 -12
  108. package/dist/self-hosted-roles.js.map +1 -1
  109. package/dist/self-hosted-upgrade.d.ts +17 -0
  110. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  111. package/dist/self-hosted-upgrade.js +16 -1
  112. package/dist/self-hosted-upgrade.js.map +1 -1
  113. package/dist/stack-contract.d.ts +6 -0
  114. package/dist/stack-contract.d.ts.map +1 -1
  115. package/dist/stack-contract.js +10 -1
  116. package/dist/stack-contract.js.map +1 -1
  117. package/dist/state-attach.d.ts +23 -0
  118. package/dist/state-attach.d.ts.map +1 -0
  119. package/dist/state-attach.js +112 -0
  120. package/dist/state-attach.js.map +1 -0
  121. package/dist/state-backend.d.ts +11 -0
  122. package/dist/state-backend.d.ts.map +1 -1
  123. package/dist/state-backend.js +20 -0
  124. package/dist/state-backend.js.map +1 -1
  125. package/dist/state-command.d.ts +20 -0
  126. package/dist/state-command.d.ts.map +1 -0
  127. package/dist/state-command.js +237 -0
  128. package/dist/state-command.js.map +1 -0
  129. package/dist/state-migrate.d.ts +121 -0
  130. package/dist/state-migrate.d.ts.map +1 -0
  131. package/dist/state-migrate.js +290 -0
  132. package/dist/state-migrate.js.map +1 -0
  133. package/dist/state-pulumi.d.ts +17 -0
  134. package/dist/state-pulumi.d.ts.map +1 -0
  135. package/dist/state-pulumi.js +218 -0
  136. package/dist/state-pulumi.js.map +1 -0
  137. package/dist/state-store.d.ts +27 -0
  138. package/dist/state-store.d.ts.map +1 -1
  139. package/dist/state-store.js +65 -0
  140. package/dist/state-store.js.map +1 -1
  141. package/dist/upgrade/converge.d.ts +6 -0
  142. package/dist/upgrade/converge.d.ts.map +1 -1
  143. package/dist/upgrade/converge.js +59 -2
  144. package/dist/upgrade/converge.js.map +1 -1
  145. package/dist/upgrade/gcp-lookup.d.ts +6 -0
  146. package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
  147. package/dist/upgrade/gcp-lookup.js +25 -0
  148. package/dist/upgrade/gcp-lookup.js.map +1 -1
  149. package/dist/upgrade/release-images.d.ts +12 -0
  150. package/dist/upgrade/release-images.d.ts.map +1 -1
  151. package/dist/upgrade/release-images.js +31 -0
  152. package/dist/upgrade/release-images.js.map +1 -1
  153. package/dist/upgrade/upgrade.d.ts +5 -0
  154. package/dist/upgrade/upgrade.d.ts.map +1 -1
  155. package/dist/upgrade/upgrade.js +28 -1
  156. package/dist/upgrade/upgrade.js.map +1 -1
  157. package/dist/upgrader-assess.d.ts +82 -0
  158. package/dist/upgrader-assess.d.ts.map +1 -0
  159. package/dist/upgrader-assess.js +116 -0
  160. package/dist/upgrader-assess.js.map +1 -0
  161. package/dist/upgrader-gcp.d.ts +17 -0
  162. package/dist/upgrader-gcp.d.ts.map +1 -0
  163. package/dist/upgrader-gcp.js +87 -0
  164. package/dist/upgrader-gcp.js.map +1 -0
  165. package/dist/upgrader-job.d.ts +131 -0
  166. package/dist/upgrader-job.d.ts.map +1 -0
  167. package/dist/upgrader-job.js +259 -0
  168. package/dist/upgrader-job.js.map +1 -0
  169. package/dist/upgrader-reporter.d.ts +35 -0
  170. package/dist/upgrader-reporter.d.ts.map +1 -0
  171. package/dist/upgrader-reporter.js +79 -0
  172. package/dist/upgrader-reporter.js.map +1 -0
  173. package/dist/upgrader-setup.d.ts +59 -0
  174. package/dist/upgrader-setup.d.ts.map +1 -0
  175. package/dist/upgrader-setup.js +308 -0
  176. package/dist/upgrader-setup.js.map +1 -0
  177. package/dist/wizard.d.ts +6 -0
  178. package/dist/wizard.d.ts.map +1 -1
  179. package/dist/wizard.js +32 -4
  180. package/dist/wizard.js.map +1 -1
  181. package/dist/wizards/gcp-firebase.d.ts +3 -0
  182. package/dist/wizards/gcp-firebase.d.ts.map +1 -1
  183. package/dist/wizards/gcp-firebase.js +1 -0
  184. package/dist/wizards/gcp-firebase.js.map +1 -1
  185. package/dist/wizards/gcp-install-job.d.ts +74 -0
  186. package/dist/wizards/gcp-install-job.d.ts.map +1 -0
  187. package/dist/wizards/gcp-install-job.js +398 -0
  188. package/dist/wizards/gcp-install-job.js.map +1 -0
  189. package/dist/wizards/gcp-prompts.d.ts +3 -0
  190. package/dist/wizards/gcp-prompts.d.ts.map +1 -0
  191. package/dist/wizards/gcp-prompts.js +25 -0
  192. package/dist/wizards/gcp-prompts.js.map +1 -0
  193. package/dist/wizards/gcp-pulumi.d.ts +9 -2
  194. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  195. package/dist/wizards/gcp-pulumi.js +8 -6
  196. package/dist/wizards/gcp-pulumi.js.map +1 -1
  197. package/dist/wizards/gcp.d.ts +40 -0
  198. package/dist/wizards/gcp.d.ts.map +1 -1
  199. package/dist/wizards/gcp.js +291 -83
  200. package/dist/wizards/gcp.js.map +1 -1
  201. package/package.json +6 -4
  202. package/src/adapter.ts +97 -0
  203. package/src/adc-impersonation.ts +176 -0
  204. package/src/admin-domain.test.ts +254 -0
  205. package/src/admin-domain.ts +245 -0
  206. package/src/admin-memory.test.ts +151 -0
  207. package/src/agent/apply.test.ts +185 -0
  208. package/src/agent/apply.ts +124 -0
  209. package/src/agent/cli-provenance.test.ts +159 -0
  210. package/src/agent/cli-provenance.ts +191 -0
  211. package/src/agent/command.test.ts +77 -0
  212. package/src/agent/command.ts +564 -0
  213. package/src/agent/config.ts +86 -0
  214. package/src/agent/lock-config.test.ts +97 -0
  215. package/src/agent/lock.ts +110 -0
  216. package/src/agent/queue.test.ts +10 -0
  217. package/src/agent/queue.ts +91 -0
  218. package/src/agent/run.test.ts +382 -0
  219. package/src/agent/run.ts +295 -0
  220. package/src/agent/systemd.ts +209 -0
  221. package/src/agent/units.test.ts +223 -0
  222. package/src/agent/units.ts +163 -0
  223. package/src/azure-front-door.test.ts +119 -0
  224. package/src/azure-front-door.ts +56 -0
  225. package/src/bootstrap-token.ts +20 -0
  226. package/src/caddy.ts +112 -0
  227. package/src/cdn-copy.ts +91 -0
  228. package/src/cert-wait.test.ts +194 -0
  229. package/src/cert-wait.ts +163 -0
  230. package/src/checkpoint-secrets.test.ts +380 -0
  231. package/src/checkpoint-secrets.ts +283 -0
  232. package/src/cli-routing.test.ts +45 -0
  233. package/src/cli-routing.ts +43 -0
  234. package/src/cli.ts +1491 -0
  235. package/src/cloudsql-iam-user.ts +147 -0
  236. package/src/compose.ts +334 -0
  237. package/src/default-firewall.test.ts +411 -0
  238. package/src/default-firewall.ts +342 -0
  239. package/src/deployed-release.test.ts +231 -0
  240. package/src/deployed-release.ts +220 -0
  241. package/src/dns/cloudflare.ts +182 -0
  242. package/src/dns/groups.test.ts +76 -0
  243. package/src/dns/groups.ts +86 -0
  244. package/src/dns/index.ts +56 -0
  245. package/src/dns/manual.ts +130 -0
  246. package/src/dns/types.ts +27 -0
  247. package/src/gateway-credentials.test.ts +180 -0
  248. package/src/gateway-credentials.ts +144 -0
  249. package/src/gcloud-components.test.ts +76 -0
  250. package/src/gcloud-components.ts +85 -0
  251. package/src/gcloud-retry.ts +41 -0
  252. package/src/gcloud-runner.test.ts +61 -0
  253. package/src/gcloud.test.ts +99 -0
  254. package/src/gcloud.ts +322 -0
  255. package/src/gcp-names.ts +135 -0
  256. package/src/index.test.ts +332 -0
  257. package/src/index.ts +114 -0
  258. package/src/infra-contract.ts +32 -0
  259. package/src/install-job-runner.ts +166 -0
  260. package/src/install-job.test.ts +566 -0
  261. package/src/install-job.ts +542 -0
  262. package/src/install-state.test.ts +239 -0
  263. package/src/install-state.ts +583 -0
  264. package/src/internal-token.ts +35 -0
  265. package/src/lifecycle.ts +900 -0
  266. package/src/mcp-iap-stacks.test.ts +58 -0
  267. package/src/mcp-iap.test.ts +149 -0
  268. package/src/mcp-iap.ts +131 -0
  269. package/src/media-volume.test.ts +296 -0
  270. package/src/migration-runner.ts +365 -0
  271. package/src/operator-access-stacks.test.ts +55 -0
  272. package/src/operator-access.test.ts +332 -0
  273. package/src/operator-access.ts +554 -0
  274. package/src/ops-backend.test.ts +320 -0
  275. package/src/ops-backend.ts +414 -0
  276. package/src/provisioner-identity.test.ts +482 -0
  277. package/src/provisioner-identity.ts +407 -0
  278. package/src/redirects-emit.ts +175 -0
  279. package/src/region-data.ts +428 -0
  280. package/src/regions.test.ts +277 -0
  281. package/src/regions.ts +370 -0
  282. package/src/release-contract.ts +57 -0
  283. package/src/release-manifest.test.ts +293 -0
  284. package/src/release-manifest.ts +430 -0
  285. package/src/run-mode.ts +40 -0
  286. package/src/runtime-secrets.test.ts +627 -0
  287. package/src/runtime-secrets.ts +754 -0
  288. package/src/self-command.test.ts +14 -0
  289. package/src/self-command.ts +21 -0
  290. package/src/self-hosted-compose.test.ts +177 -0
  291. package/src/self-hosted-release.ts +240 -0
  292. package/src/self-hosted-roles.integration.test.ts +161 -0
  293. package/src/self-hosted-roles.test.ts +124 -0
  294. package/src/self-hosted-roles.ts +203 -0
  295. package/src/self-hosted-upgrade.test.ts +394 -0
  296. package/src/self-hosted-upgrade.ts +548 -0
  297. package/src/stack-contract-parity.test.ts +199 -0
  298. package/src/stack-contract.test.ts +391 -0
  299. package/src/stack-contract.ts +888 -0
  300. package/src/stack-converge-secrets.test.ts +421 -0
  301. package/src/stack-converge.test.ts +833 -0
  302. package/src/stack-converge.ts +909 -0
  303. package/src/staging-admin.test.ts +246 -0
  304. package/src/staging-admin.ts +324 -0
  305. package/src/state-attach.test.ts +211 -0
  306. package/src/state-attach.ts +132 -0
  307. package/src/state-backend.test.ts +145 -0
  308. package/src/state-backend.ts +262 -0
  309. package/src/state-command.ts +293 -0
  310. package/src/state-migrate.test.ts +479 -0
  311. package/src/state-migrate.ts +434 -0
  312. package/src/state-pulumi.ts +289 -0
  313. package/src/state-store.test.ts +212 -0
  314. package/src/state-store.ts +243 -0
  315. package/src/upgrade/app-roll.ts +486 -0
  316. package/src/upgrade/backup.ts +109 -0
  317. package/src/upgrade/converge.ts +437 -0
  318. package/src/upgrade/gcp-lookup.ts +167 -0
  319. package/src/upgrade/health.test.ts +279 -0
  320. package/src/upgrade/health.ts +305 -0
  321. package/src/upgrade/release-images.test.ts +119 -0
  322. package/src/upgrade/release-images.ts +391 -0
  323. package/src/upgrade/reporter.ts +46 -0
  324. package/src/upgrade/upgrade.test.ts +626 -0
  325. package/src/upgrade/upgrade.ts +249 -0
  326. package/src/upgrader-assess.test.ts +265 -0
  327. package/src/upgrader-assess.ts +233 -0
  328. package/src/upgrader-gcp.ts +123 -0
  329. package/src/upgrader-job.test.ts +408 -0
  330. package/src/upgrader-job.ts +499 -0
  331. package/src/upgrader-reporter.test.ts +46 -0
  332. package/src/upgrader-reporter.ts +105 -0
  333. package/src/upgrader-setup.test.ts +169 -0
  334. package/src/upgrader-setup.ts +355 -0
  335. package/src/wizard.ts +425 -0
  336. package/src/wizards/gcp-cost.ts +188 -0
  337. package/src/wizards/gcp-firebase.ts +72 -0
  338. package/src/wizards/gcp-install-job.ts +506 -0
  339. package/src/wizards/gcp-prompts.ts +26 -0
  340. package/src/wizards/gcp-pulumi.test.ts +246 -0
  341. package/src/wizards/gcp-pulumi.ts +516 -0
  342. package/src/wizards/gcp.ts +1935 -0
@@ -0,0 +1,147 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * A Cloud SQL IAM database user for one of the install's in-cloud jobs
5
+ * (the operator-access sync job, the upgrader job): IAM database
6
+ * authentication switched on for the instance, the job's service account
7
+ * as a `cloud_iam_service_account` user, and that user made a member of
8
+ * exactly one database role a migration created for it. No password ever
9
+ * exists; the job logs in with its short-lived access token.
10
+ */
11
+
12
+ import type { GcloudResult } from "./gcloud.js";
13
+ import { gcpNamePrefix } from "./gcp-names.js";
14
+
15
+ type Run = (args: string[]) => Promise<GcloudResult>;
16
+
17
+ /** The flags of a Cloud SQL instance with `cloudsql.iam_authentication` on. */
18
+ export function withIamAuthFlag(
19
+ flags: readonly { name: string; value: string }[],
20
+ ): { name: string; value: string }[] | null {
21
+ if (flags.some((f) => f.name === "cloudsql.iam_authentication" && f.value === "on")) return null;
22
+ return [
23
+ ...flags.filter((f) => f.name !== "cloudsql.iam_authentication"),
24
+ { name: "cloudsql.iam_authentication", value: "on" },
25
+ ];
26
+ }
27
+
28
+ export type CloudSqlIamUserResult =
29
+ | { readonly ok: true; readonly done: string[] }
30
+ | { readonly ok: false; readonly done: string[]; readonly error: string };
31
+
32
+ /**
33
+ * Ensure `serviceAccountEmail` logs in to the install's Cloud SQL instance
34
+ * as an IAM database user holding `databaseRole`. Idempotent; never throws
35
+ * for a gcloud failure.
36
+ */
37
+ export async function ensureCloudSqlIamUser(
38
+ run: Run,
39
+ opts: {
40
+ readonly projectId: string;
41
+ readonly env: string;
42
+ readonly serviceAccountEmail: string;
43
+ readonly databaseRole: string;
44
+ },
45
+ ): Promise<CloudSqlIamUserResult> {
46
+ const project = `--project=${opts.projectId}`;
47
+ const done: string[] = [];
48
+ const fail = (step: string, r: GcloudResult): CloudSqlIamUserResult => ({
49
+ ok: false,
50
+ done,
51
+ error: `${step}: ${r.stderr.trim()}`,
52
+ });
53
+ const instances = await run([
54
+ "sql",
55
+ "instances",
56
+ "list",
57
+ project,
58
+ `--filter=name~^${gcpNamePrefix(opts.env)}-pg`,
59
+ "--format=value(name)",
60
+ ]);
61
+ if (!instances.ok) return fail("list Cloud SQL instances", instances);
62
+ const names = instances.stdout
63
+ .split("\n")
64
+ .map((s) => s.trim())
65
+ .filter(Boolean);
66
+ if (names.length !== 1) {
67
+ return {
68
+ ok: false,
69
+ done,
70
+ error: `expected exactly one ${gcpNamePrefix(opts.env)}-pg* Cloud SQL instance, found ${names.length} (${names.join(", ")})`,
71
+ };
72
+ }
73
+ const instance = names[0] as string;
74
+ const flagsJson = await run([
75
+ "sql",
76
+ "instances",
77
+ "describe",
78
+ instance,
79
+ project,
80
+ "--format=json(settings.databaseFlags)",
81
+ ]);
82
+ if (!flagsJson.ok) return fail(`read the flags of ${instance}`, flagsJson);
83
+ const flags =
84
+ (
85
+ JSON.parse(flagsJson.stdout) as {
86
+ settings?: { databaseFlags?: { name: string; value: string }[] };
87
+ }
88
+ ).settings?.databaseFlags ?? [];
89
+ const nextFlags = withIamAuthFlag(flags);
90
+ if (nextFlags) {
91
+ // `--database-flags` replaces the whole list, so every existing flag is
92
+ // passed again. cloudsql.iam_authentication needs no restart.
93
+ const patch = await run([
94
+ "sql",
95
+ "instances",
96
+ "patch",
97
+ instance,
98
+ project,
99
+ `--database-flags=${nextFlags.map((f) => `${f.name}=${f.value}`).join(",")}`,
100
+ "--quiet",
101
+ ]);
102
+ if (!patch.ok) return fail(`enable IAM database authentication on ${instance}`, patch);
103
+ done.push(`${instance}: cloudsql.iam_authentication=on`);
104
+ }
105
+ const dbUser = opts.serviceAccountEmail.replace(/\.gserviceaccount\.com$/, "");
106
+ const users = await run([
107
+ "sql",
108
+ "users",
109
+ "list",
110
+ `--instance=${instance}`,
111
+ project,
112
+ "--format=value(name)",
113
+ ]);
114
+ if (!users.ok) return fail(`list database users of ${instance}`, users);
115
+ if (
116
+ !users.stdout
117
+ .split("\n")
118
+ .map((s) => s.trim())
119
+ .includes(dbUser)
120
+ ) {
121
+ const create = await run([
122
+ "sql",
123
+ "users",
124
+ "create",
125
+ dbUser,
126
+ `--instance=${instance}`,
127
+ "--type=cloud_iam_service_account",
128
+ project,
129
+ "--quiet",
130
+ ]);
131
+ if (!create.ok) return fail(`create database user ${dbUser}`, create);
132
+ }
133
+ const assign = await run([
134
+ "sql",
135
+ "users",
136
+ "assign-roles",
137
+ dbUser,
138
+ `--instance=${instance}`,
139
+ "--type=cloud_iam_service_account",
140
+ `--database-roles=${opts.databaseRole}`,
141
+ project,
142
+ "--quiet",
143
+ ]);
144
+ if (!assign.ok) return fail(`give ${dbUser} the ${opts.databaseRole} database role`, assign);
145
+ done.push(`database user ${dbUser} (${opts.databaseRole})`);
146
+ return { ok: true, done };
147
+ }
package/src/compose.ts ADDED
@@ -0,0 +1,334 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * P14 — production docker-compose.yml generator.
5
+ *
6
+ * Emits the production stack: Postgres + pgBackRest sidecar + Caddy +
7
+ * MinIO + the two Caelo services (admin, gateway). Idempotent: same input →
8
+ * byte-identical output. The CLI's `init` / `up` / `upgrade` sub-commands
9
+ * write this to `.caelo/docker-compose.yml`.
10
+ *
11
+ * #696 — the admin and gateway run the PREBUILT, cosign-verified release
12
+ * images, pinned by digest (CLAUDE.md §11.C: "a PR that requires the user to
13
+ * run docker build is a regression"). Nothing of a Caelo source checkout is
14
+ * mounted. The digest pin is what `upgrade` moves; a tag would let a
15
+ * `docker compose pull` change the running release behind the operator's
16
+ * back. Contributors who want their working tree in the containers ask for
17
+ * {@link DevImages} explicitly (`init --dev`) — never the default.
18
+ */
19
+
20
+ import { ADMIN_MEMORY_DEFAULT, memoryQuantityMiB, publicSiteUrl } from "./stack-contract.js";
21
+ import { ghcrRepository, isImageDigest, type ReleaseService } from "./upgrade/release-images.js";
22
+
23
+ /** The admin + gateway release images, each `ghcr.io/caelo-cms/<svc>@sha256:…`. */
24
+ export interface ReleaseImages {
25
+ readonly mode: "release";
26
+ readonly admin: string;
27
+ readonly gateway: string;
28
+ }
29
+
30
+ /**
31
+ * Contributor-only: run the admin + gateway from the Caelo checkout the
32
+ * `.caelo/` dir lives in (bind mount on `oven/bun`). Never for an end-user
33
+ * install — it needs the repo and builds nothing reproducible.
34
+ */
35
+ export interface DevImages {
36
+ readonly mode: "dev";
37
+ }
38
+
39
+ export type ComposeImages = ReleaseImages | DevImages;
40
+
41
+ /** The admin's HTTP port inside the Compose network (the Caddyfile proxies to it). */
42
+ export const COMPOSE_ADMIN_PORT = 5173;
43
+ /** `.caelo/<this>/` holds the pending Owner-bootstrap token the admin consumes. */
44
+ export const BOOTSTRAP_DIR = "bootstrap";
45
+
46
+ /**
47
+ * Where the host's `.caelo/output/` appears in the admin (read-write, the
48
+ * generator's output root) and in Caddy (read-only). The seeded deploy
49
+ * targets' `out_dir` (`output/<env>`) resolves below it, which is where the
50
+ * Caddyfile's site roots point (`/srv/caelo/output/<env>/current`).
51
+ */
52
+ export const SITE_OUTPUT_ROOT = "/srv/caelo";
53
+
54
+ /** `.caelo/<this>/` holds the uploaded media (the admin's local media adapter root). */
55
+ export const MEDIA_DIR = "media";
56
+ /** Where {@link MEDIA_DIR} appears in the admin container (its `MEDIA_ROOT_DIR`). */
57
+ export const MEDIA_ROOT = "/data/media";
58
+
59
+ /** The gateway's HTTP port inside the Compose network. */
60
+ export const COMPOSE_GATEWAY_PORT = 8090;
61
+
62
+ /**
63
+ * `ghcr.io/caelo-cms/<service>@<digest>` — the only image reference shape a
64
+ * release-mode Compose file accepts.
65
+ */
66
+ export function ghcrImageRef(service: ReleaseService, digest: string): string {
67
+ if (!isImageDigest(digest)) throw new Error(`not an image digest: ${JSON.stringify(digest)}`);
68
+ return `${ghcrRepository(service)}@${digest}`;
69
+ }
70
+
71
+ /**
72
+ * The digest of a release image reference, or null when `ref` is not a
73
+ * digest-pinned reference to that service's Caelo repository.
74
+ */
75
+ export function ghcrImageDigest(service: ReleaseService, ref: string): string | null {
76
+ const prefix = `${ghcrRepository(service)}@`;
77
+ if (!ref.startsWith(prefix)) return null;
78
+ const digest = ref.slice(prefix.length);
79
+ return isImageDigest(digest) ? digest : null;
80
+ }
81
+
82
+ /**
83
+ * The application roles' passwords (bootstrap.sh creates the roles with
84
+ * them). One per role, so the gateway — which gets only the public and
85
+ * gateway logins — never holds admin_role's (#613, CLAUDE.md §2).
86
+ */
87
+ export interface RolePasswords {
88
+ readonly admin: string;
89
+ readonly public: string;
90
+ readonly gateway: string;
91
+ }
92
+
93
+ export interface ComposeSpec {
94
+ readonly domain: string;
95
+ /** The Postgres superuser's; only the database container uses it. */
96
+ readonly postgresPassword: string; // generated by cms-provision; persisted in .caelo/secrets
97
+ readonly rolePasswords: RolePasswords;
98
+ readonly minioRootUser: string;
99
+ readonly minioRootPassword: string;
100
+ /**
101
+ * P18 — project KEK (32 bytes hex) for at-rest encryption of AI provider
102
+ * keys + future secrets. Generated per install by cms-provision; persisted
103
+ * in `.caelo/secrets`. NEVER prompt the operator for this — auto-generated
104
+ * via the same randomHex(32) the GCP stack uses.
105
+ */
106
+ readonly caeloSecretKek: string;
107
+ readonly anthropicApiKey?: string;
108
+ readonly resendApiKey?: string;
109
+ readonly diskSize: string;
110
+ /**
111
+ * #553 — memory reserved for the admin (it runs the Lighthouse quality
112
+ * audit). Same `adminMemory` knob + default as every adapter
113
+ * (stack-contract.ts); compose reserves it rather than capping it, so a
114
+ * busy host never OOM-kills the admin at the limit.
115
+ */
116
+ readonly adminMemory?: string;
117
+ /** Which admin + gateway images run (see {@link ComposeImages}). */
118
+ readonly images: ComposeImages;
119
+ /**
120
+ * #696 Phase 4c — the host update agent's database login, when the agent
121
+ * is set up: Postgres is then also published on `127.0.0.1:<dbPort>` (never
122
+ * on another interface) for the agent on the host, and bootstrap.sh
123
+ * creates the `caelo_agent` role from `AGENT_ROLE_PASSWORD`.
124
+ */
125
+ readonly agent?: ComposeAgent;
126
+ }
127
+
128
+ /** The host agent's side of the Compose file (see {@link ComposeSpec.agent}). */
129
+ export interface ComposeAgent {
130
+ readonly dbPort: number;
131
+ readonly dbPassword: string;
132
+ }
133
+
134
+ /** The only interface the agent's Postgres port is published on. */
135
+ export const AGENT_DB_BIND_ADDRESS = "127.0.0.1";
136
+
137
+ export function generateDockerCompose(spec: ComposeSpec): string {
138
+ const adminMemory = spec.adminMemory ?? ADMIN_MEMORY_DEFAULT;
139
+ const adminMemoryMiB = memoryQuantityMiB(adminMemory);
140
+ if (adminMemoryMiB === null) {
141
+ throw new Error(`adminMemory "${adminMemory}" is not a memory quantity (e.g. 2Gi)`);
142
+ }
143
+ const env = (k: string, v: string | undefined): string =>
144
+ v ? ` ${k}: "${escapeYaml(v)}"` : "";
145
+ if (spec.agent && !isAgentDbPort(spec.agent.dbPort)) {
146
+ throw new Error(`agent dbPort ${spec.agent.dbPort} is not a TCP port between 1024 and 65535`);
147
+ }
148
+ return `# SPDX-License-Identifier: MPL-2.0
149
+ # Generated by cms-provision — do not edit; re-run \`bunx cms-provision up\`.
150
+ # Domain: ${spec.domain}
151
+
152
+ services:
153
+ postgres:
154
+ image: postgres:16-alpine
155
+ container_name: caelo-postgres
156
+ restart: unless-stopped
157
+ environment:
158
+ POSTGRES_PASSWORD: "${escapeYaml(spec.postgresPassword)}"
159
+ POSTGRES_USER: caelo
160
+ POSTGRES_DB: caelo
161
+ ADMIN_ROLE_PASSWORD: "${escapeYaml(spec.rolePasswords.admin)}"
162
+ PUBLIC_ROLE_PASSWORD: "${escapeYaml(spec.rolePasswords.public)}"
163
+ GATEWAY_ROLE_PASSWORD: "${escapeYaml(spec.rolePasswords.gateway)}"
164
+ ${postgresAgent(spec.agent)} volumes:
165
+ - caelo-pg:/var/lib/postgresql/data
166
+ - ./bootstrap.sh:/docker-entrypoint-initdb.d/00_bootstrap.sh:ro
167
+ healthcheck:
168
+ test: ["CMD-SHELL", "pg_isready -U caelo"]
169
+ interval: 10s
170
+ timeout: 5s
171
+ retries: 5
172
+
173
+ pgbackrest:
174
+ image: pgbackrest/pgbackrest:latest
175
+ container_name: caelo-pgbackrest
176
+ restart: unless-stopped
177
+ depends_on: [postgres]
178
+ volumes:
179
+ - caelo-pg-backups:/var/lib/pgbackrest
180
+ - ./pgbackrest.conf:/etc/pgbackrest.conf:ro
181
+
182
+ minio:
183
+ image: minio/minio:latest
184
+ container_name: caelo-minio
185
+ restart: unless-stopped
186
+ command: server /data --console-address ":9001"
187
+ environment:
188
+ MINIO_ROOT_USER: "${escapeYaml(spec.minioRootUser)}"
189
+ MINIO_ROOT_PASSWORD: "${escapeYaml(spec.minioRootPassword)}"
190
+ volumes:
191
+ - caelo-minio:/data
192
+
193
+ caddy:
194
+ image: caddy:2-alpine
195
+ container_name: caelo-caddy
196
+ restart: unless-stopped
197
+ ports: ["80:80", "443:443"]
198
+ volumes:
199
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
200
+ - caelo-caddy-data:/data
201
+ - caelo-caddy-config:/config
202
+ - ./output:${SITE_OUTPUT_ROOT}:ro
203
+
204
+ ${adminService(spec, adminMemoryMiB, env)}
205
+
206
+ ${gatewayService(spec)}
207
+
208
+ volumes:
209
+ caelo-pg:
210
+ caelo-pg-backups:
211
+ caelo-minio:
212
+ caelo-caddy-data:
213
+ caelo-caddy-config:
214
+ `;
215
+ }
216
+
217
+ type EnvLine = (k: string, v: string | undefined) => string;
218
+
219
+ /** Whether `port` may carry the agent's Postgres (unprivileged TCP port). */
220
+ export function isAgentDbPort(port: number): boolean {
221
+ return Number.isInteger(port) && port >= 1024 && port <= 65535;
222
+ }
223
+
224
+ /**
225
+ * The agent's lines in the postgres service: its role password for
226
+ * bootstrap.sh, and the port on the host's loopback interface only — the
227
+ * agent runs on the host, and nothing else may reach the database.
228
+ */
229
+ function postgresAgent(agent: ComposeAgent | undefined): string {
230
+ if (!agent) return "";
231
+ return [
232
+ ` AGENT_ROLE_PASSWORD: "${escapeYaml(agent.dbPassword)}"`,
233
+ " # The host update agent (cms-provision agent): loopback only.",
234
+ ` ports: ["${AGENT_DB_BIND_ADDRESS}:${agent.dbPort}:5432"]`,
235
+ "",
236
+ ].join("\n");
237
+ }
238
+
239
+ /**
240
+ * How a Caelo service's container starts: its image and, in dev mode, the
241
+ * checkout. `mounts` are extra bind mounts the service needs in both modes.
242
+ */
243
+ function serviceRuntime(
244
+ images: ComposeImages,
245
+ service: ReleaseService,
246
+ devCommand: string,
247
+ mounts: readonly string[] = [],
248
+ ): string {
249
+ const volumes = (list: readonly string[]) =>
250
+ list.length === 0 ? [] : [" volumes:", ...list.map((m) => ` - ${m}`)];
251
+ if (images.mode === "dev") {
252
+ return [
253
+ " # DEV MODE (cms-provision init --dev): runs the Caelo checkout this",
254
+ " # .caelo/ dir lives in. Contributors only — never an end-user install.",
255
+ " image: oven/bun:1.4.2",
256
+ " working_dir: /app",
257
+ ` command: ${devCommand}`,
258
+ ...volumes(["../..:/app", ...mounts]),
259
+ ].join("\n");
260
+ }
261
+ const digest = ghcrImageDigest(service, images[service]);
262
+ if (!digest) {
263
+ throw new Error(
264
+ `${service} image ${JSON.stringify(images[service])} is not ${ghcrRepository(service)}@sha256:<digest>`,
265
+ );
266
+ }
267
+ // The image's own CMD starts the service; no source is mounted.
268
+ return [` image: ${ghcrImageRef(service, digest)}`, ...volumes(mounts)].join("\n");
269
+ }
270
+
271
+ function adminService(spec: ComposeSpec, adminMemoryMiB: number, env: EnvLine): string {
272
+ return ` caelo-admin:
273
+ ${serviceRuntime(
274
+ spec.images,
275
+ "admin",
276
+ `sh -c "cd apps/admin && bunx svelte-kit sync && bun --bun vite dev --host 0.0.0.0 --port ${COMPOSE_ADMIN_PORT}"`,
277
+ // The Owner-bootstrap token handoff: `init` writes the token to
278
+ // `.caelo/bootstrap/pending-token.json`; the admin reads (and deletes) it
279
+ // from `/app/.caelo/` on first boot (hooks.server.ts). Only that dir is
280
+ // mounted — never `.caelo/` itself, which holds config.json's superuser
281
+ // and MinIO passwords. Without the token /setup accepts any first Owner.
282
+ // The static site: the admin builds into it (CAELO_OUTPUT_ROOT below),
283
+ // Caddy serves it read-only from the same host dir. On the host, so a
284
+ // recreated admin (upgrade) keeps the published site.
285
+ // Uploaded media (MEDIA_ROOT_DIR below) likewise lives on the host.
286
+ [
287
+ `./${BOOTSTRAP_DIR}:/app/.caelo`,
288
+ `./output:${SITE_OUTPUT_ROOT}`,
289
+ `./${MEDIA_DIR}:${MEDIA_ROOT}`,
290
+ ],
291
+ )}
292
+ container_name: caelo-admin
293
+ restart: unless-stopped
294
+ depends_on:
295
+ postgres: { condition: service_healthy }
296
+ mem_reservation: ${adminMemoryMiB}m
297
+ environment:
298
+ ADMIN_DATABASE_URL: "${dbUrl("admin_role", spec.rolePasswords.admin, "cms_admin")}"
299
+ PUBLIC_ADMIN_DATABASE_URL: "${dbUrl("admin_role", spec.rolePasswords.admin, "cms_public")}"
300
+ CAELO_SECRET_KEK: "${escapeYaml(spec.caeloSecretKek)}"
301
+ CAELO_SITE_URL: "${escapeYaml(publicSiteUrl(spec.domain))}"
302
+ PORT: "${COMPOSE_ADMIN_PORT}"
303
+ CAELO_OUTPUT_ROOT: "${SITE_OUTPUT_ROOT}"
304
+ MEDIA_ROOT_DIR: "${MEDIA_ROOT}"
305
+ CAELO_PROVIDER: "self-hosted"
306
+ NODE_ENV: production
307
+ ${env("ANTHROPIC_API_KEY", spec.anthropicApiKey)}
308
+ ${env("RESEND_API_KEY", spec.resendApiKey)}`;
309
+ }
310
+
311
+ function gatewayService(spec: ComposeSpec): string {
312
+ return ` caelo-gateway:
313
+ ${serviceRuntime(spec.images, "gateway", "bun run apps/api-gateway/src/server.ts")}
314
+ container_name: caelo-gateway
315
+ restart: unless-stopped
316
+ depends_on:
317
+ postgres: { condition: service_healthy }
318
+ environment:
319
+ # Its own two logins only — no authoring credential (#613, CLAUDE.md §2).
320
+ GATEWAY_DATABASE_URL: "${dbUrl("gateway_role", spec.rolePasswords.gateway, "cms_admin")}"
321
+ PUBLIC_DATABASE_URL: "${dbUrl("public_role", spec.rolePasswords.public, "cms_public")}"
322
+ GATEWAY_PORT: "${COMPOSE_GATEWAY_PORT}"
323
+ NODE_ENV: production`;
324
+ }
325
+
326
+ /** A compose-network database URL, password URL-encoded and YAML-escaped. */
327
+ function dbUrl(role: string, password: string, database: string): string {
328
+ return escapeYaml(`postgres://${role}:${encodeURIComponent(password)}@postgres:5432/${database}`);
329
+ }
330
+
331
+ function escapeYaml(s: string): string {
332
+ // Conservative escape for double-quoted YAML strings.
333
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
334
+ }