@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,124 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import {
5
+ convergeSelfHostedRoles,
6
+ decideUp,
7
+ roleConvergenceScript,
8
+ selfHostedBootstrapScript,
9
+ } from "./self-hosted-roles.js";
10
+
11
+ const PASSWORDS = { admin: "a'pw", public: "p-pw", gateway: "g-pw" };
12
+
13
+ describe("roleConvergenceScript (#613 review)", () => {
14
+ const script = roleConvergenceScript(PASSWORDS);
15
+
16
+ it("creates missing roles and (re)sets every role's password, quoted", () => {
17
+ for (const role of ["admin_role", "public_role", "gateway_role"]) {
18
+ expect(script).toContain(
19
+ `WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${role}')`,
20
+ );
21
+ }
22
+ expect(script).toContain("ALTER ROLE admin_role WITH LOGIN PASSWORD 'a''pw';");
23
+ expect(script).toContain("ALTER ROLE gateway_role WITH LOGIN PASSWORD 'g-pw';");
24
+ });
25
+
26
+ it("re-applies migration 0248's gateway grants in cms_admin, and stops on the first error", () => {
27
+ expect(script.startsWith("\\set ON_ERROR_STOP on")).toBe(true);
28
+ expect(script.indexOf("\\connect cms_admin")).toBeLessThan(
29
+ script.indexOf("PERFORM caelo_grant_gateway_role()"),
30
+ );
31
+ });
32
+ });
33
+
34
+ describe("the host agent's login (#696 Phase 4c)", () => {
35
+ it("is created only with a password, inherits platform_updater and nothing else", () => {
36
+ expect(roleConvergenceScript(PASSWORDS)).not.toContain("caelo_agent");
37
+ const script = roleConvergenceScript(PASSWORDS, "ag'pw");
38
+ expect(script).toContain(
39
+ "ALTER ROLE caelo_agent WITH INHERIT LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD 'ag''pw';",
40
+ );
41
+ expect(script).toContain("'GRANT platform_updater TO caelo_agent' WHERE NOT pg_has_role(");
42
+ // platform_updater exists before the grant, and its table grants are
43
+ // re-applied in cms_admin (migrations 0267 + 0270).
44
+ expect(script.indexOf("CREATE ROLE platform_updater NOLOGIN")).toBeLessThan(
45
+ script.indexOf("GRANT platform_updater TO caelo_agent"),
46
+ );
47
+ expect(script.indexOf("\\connect cms_admin")).toBeLessThan(
48
+ script.indexOf("PERFORM caelo_grant_platform_updater()"),
49
+ );
50
+ });
51
+ });
52
+
53
+ describe("selfHostedBootstrapScript — what Postgres runs on its first start", () => {
54
+ it("holds no secret: every password comes from the container's environment", () => {
55
+ const script = selfHostedBootstrapScript({ agent: true });
56
+ expect(script.startsWith("#!/bin/sh\n")).toBe(true);
57
+ expect(script).toContain('-v caelo_agent_pw="$AGENT_ROLE_PASSWORD"');
58
+ expect(script).toContain("ALTER ROLE admin_role WITH LOGIN PASSWORD :'admin_role_pw';");
59
+ expect(script).toContain(`: "\${AGENT_ROLE_PASSWORD:?must be set in environment}"`);
60
+ // The SQL heredoc is quoted, so the shell expands nothing inside it.
61
+ expect(script).toContain("<<'CAELO_SQL'");
62
+ expect(script.trimEnd().endsWith("CAELO_SQL")).toBe(true);
63
+ });
64
+
65
+ it("leaves the agent out when the install has none", () => {
66
+ const script = selfHostedBootstrapScript({ agent: false });
67
+ expect(script).not.toContain("AGENT_ROLE_PASSWORD");
68
+ expect(script).not.toContain("caelo_agent");
69
+ expect(script).toContain("CREATE ROLE platform_updater NOLOGIN");
70
+ });
71
+ });
72
+
73
+ describe("convergeSelfHostedRoles", () => {
74
+ it("does nothing while Postgres is down", async () => {
75
+ let ran = false;
76
+ const r = await convergeSelfHostedRoles(PASSWORDS, {
77
+ probe: async () => false,
78
+ psql: async () => {
79
+ ran = true;
80
+ return { exitCode: 0, stderr: "" };
81
+ },
82
+ });
83
+ expect(r).toEqual({
84
+ ok: false,
85
+ reachable: false,
86
+ error: "the Postgres container is not running",
87
+ });
88
+ expect(ran).toBe(false);
89
+ });
90
+
91
+ it("hands the script to psql on stdin and reports its failure", async () => {
92
+ const scripts: string[] = [];
93
+ const r = await convergeSelfHostedRoles(PASSWORDS, {
94
+ probe: async () => true,
95
+ psql: async (script) => {
96
+ scripts.push(script);
97
+ return { exitCode: 3, stderr: "ERROR: permission denied\n" };
98
+ },
99
+ });
100
+ expect(scripts).toEqual([roleConvergenceScript(PASSWORDS)]);
101
+ expect(r).toEqual({ ok: false, reachable: true, error: "ERROR: permission denied" });
102
+ });
103
+ });
104
+
105
+ describe("decideUp — `cms-provision up` never switches URLs onto unapplied passwords", () => {
106
+ it("a back-filled config converges, then proceeds and records it", () => {
107
+ expect(decideUp(false, { ok: true })).toEqual({ kind: "proceed", markConverged: true });
108
+ });
109
+
110
+ it("a back-filled config with Postgres down aborts before writing anything", () => {
111
+ const d = decideUp(false, { ok: false, reachable: false, error: "down" });
112
+ expect(d.kind).toBe("abort");
113
+ expect(d.kind === "abort" && d.error).toContain("up -d postgres");
114
+ });
115
+
116
+ it("a failed convergence aborts whatever the config says", () => {
117
+ expect(decideUp(true, { ok: false, reachable: true, error: "boom" }).kind).toBe("abort");
118
+ });
119
+
120
+ it("an already converged config proceeds with a warning while Postgres is down", () => {
121
+ const d = decideUp(true, { ok: false, reachable: false, error: "down" });
122
+ expect(d).toMatchObject({ kind: "proceed", markConverged: false });
123
+ });
124
+ });
@@ -0,0 +1,203 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * Converge a self-hosted install's database roles onto `.caelo/config.json`
5
+ * (issue #613, CLAUDE.md §11.C idempotent re-runs).
6
+ *
7
+ * bootstrap.sh creates the application roles, but only on the Postgres
8
+ * container's FIRST start (docker-entrypoint-initdb.d). An install created
9
+ * before the per-role passwords has a data volume whose roles have other
10
+ * passwords — and no `gateway_role` at all — so emitting Compose URLs with
11
+ * the new passwords would lock the admin and the gateway out on the next
12
+ * restart. `cms-provision up` therefore runs {@link roleConvergenceScript}
13
+ * through the running container as the Postgres superuser BEFORE it writes
14
+ * the new Compose file: it creates missing roles and databases, sets each
15
+ * role's password to the configured one, and re-applies migration 0248's
16
+ * gateway grants. Every statement is idempotent.
17
+ *
18
+ * Passwords travel on psql's stdin, never on argv.
19
+ */
20
+
21
+ import type { RolePasswords } from "./compose.js";
22
+
23
+ /** A SQL string literal. */
24
+ function literal(s: string): string {
25
+ return `'${s.replace(/'/g, "''")}'`;
26
+ }
27
+
28
+ /**
29
+ * #696 Phase 4c — the host update agent's database login. A member of
30
+ * `platform_updater` (migration 0267), so it holds exactly the upgrader's
31
+ * grants: read requests + policy, move a request through its run columns,
32
+ * append events, write the heartbeat (0270) and its own audit rows.
33
+ */
34
+ export const AGENT_DB_ROLE = "caelo_agent";
35
+
36
+ /** The application roles bootstrap.sh and the convergence create. */
37
+ const APP_ROLES = ["admin_role", "public_role", "gateway_role"] as const;
38
+ type AppRole = (typeof APP_ROLES)[number];
39
+
40
+ /** How a role's password appears in the SQL: a literal, or a psql variable. */
41
+ type PasswordSql = (role: AppRole | typeof AGENT_DB_ROLE) => string;
42
+
43
+ /**
44
+ * The convergence SQL (psql, as the superuser, connected to any database).
45
+ * `password` renders each role's password; `agent` adds the agent's login.
46
+ */
47
+ function convergenceSql(password: PasswordSql, agent: boolean): string {
48
+ return [
49
+ "\\set ON_ERROR_STOP on",
50
+ ...APP_ROLES.flatMap((role) => [
51
+ `SELECT 'CREATE ROLE ${role} NOINHERIT LOGIN' WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${role}') \\gexec`,
52
+ `ALTER ROLE ${role} WITH LOGIN PASSWORD ${password(role)};`,
53
+ ]),
54
+ // The upgrader's role (migration 0267 creates it only with CREATEROLE,
55
+ // which the self-hosted admin_role lacks).
56
+ "SELECT 'CREATE ROLE platform_updater NOLOGIN' WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'platform_updater') \\gexec",
57
+ ...(agent
58
+ ? [
59
+ // INHERIT: the agent acts with platform_updater's privileges
60
+ // without SET ROLE, like the Cloud SQL IAM user of the GCP job.
61
+ `SELECT 'CREATE ROLE ${AGENT_DB_ROLE} INHERIT LOGIN' WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${AGENT_DB_ROLE}') \\gexec`,
62
+ `ALTER ROLE ${AGENT_DB_ROLE} WITH INHERIT LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD ${password(AGENT_DB_ROLE)};`,
63
+ `SELECT 'GRANT platform_updater TO ${AGENT_DB_ROLE}' WHERE NOT pg_has_role('${AGENT_DB_ROLE}', 'platform_updater', 'MEMBER') \\gexec`,
64
+ ]
65
+ : []),
66
+ "SELECT 'CREATE DATABASE cms_admin OWNER admin_role' WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'cms_admin') \\gexec",
67
+ "SELECT 'CREATE DATABASE cms_public OWNER admin_role' WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'cms_public') \\gexec",
68
+ "REVOKE ALL ON DATABASE cms_admin FROM PUBLIC;",
69
+ "REVOKE ALL ON DATABASE cms_public FROM PUBLIC;",
70
+ "GRANT CONNECT ON DATABASE cms_admin TO admin_role;",
71
+ "GRANT CONNECT ON DATABASE cms_public TO public_role, admin_role;",
72
+ "\\connect cms_admin",
73
+ // A database that ran migration 0248 before gateway_role existed has no
74
+ // grants for it yet; one that has not run it yet gets them from it.
75
+ "DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'caelo_grant_gateway_role') THEN PERFORM caelo_grant_gateway_role(); END IF; END $$;",
76
+ // Migration 0251's staging-gate grants (issue #650), same convergence.
77
+ "DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'caelo_grant_gateway_staging_access') THEN PERFORM caelo_grant_gateway_staging_access(); END IF; END $$;",
78
+ // Migrations 0267 + 0270 (#696): the upgrader role's grants, applied
79
+ // now that the role exists.
80
+ "DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'caelo_grant_platform_updater') THEN PERFORM caelo_grant_platform_updater(); END IF; END $$;",
81
+ "",
82
+ ].join("\n");
83
+ }
84
+
85
+ /**
86
+ * The psql script (run as the superuser, connected to any database) that
87
+ * brings roles, databases and the gateway's grants to the configured state.
88
+ * Mirrors bootstrap.sh, plus the password reset bootstrap.sh never does.
89
+ * With `agentPassword`, also the host agent's login (#696 Phase 4c).
90
+ */
91
+ export function roleConvergenceScript(passwords: RolePasswords, agentPassword?: string): string {
92
+ const byRole: Record<AppRole, string> = {
93
+ admin_role: passwords.admin,
94
+ public_role: passwords.public,
95
+ gateway_role: passwords.gateway,
96
+ };
97
+ return convergenceSql(
98
+ (role) => literal(role === AGENT_DB_ROLE ? (agentPassword ?? "") : byRole[role]),
99
+ agentPassword !== undefined,
100
+ );
101
+ }
102
+
103
+ /** The env var bootstrap.sh reads each role's password from (compose.ts sets them). */
104
+ const PASSWORD_ENV: Record<AppRole | typeof AGENT_DB_ROLE, string> = {
105
+ admin_role: "ADMIN_ROLE_PASSWORD",
106
+ public_role: "PUBLIC_ROLE_PASSWORD",
107
+ gateway_role: "GATEWAY_ROLE_PASSWORD",
108
+ [AGENT_DB_ROLE]: "AGENT_ROLE_PASSWORD",
109
+ };
110
+
111
+ /**
112
+ * `.caelo/bootstrap.sh` — what the Postgres container runs on its first
113
+ * start (docker-entrypoint-initdb.d): the same convergence SQL, with every
114
+ * password read from the container's environment (psql variables), so the
115
+ * file holds no secret and can be world-readable for the postgres user.
116
+ *
117
+ * @param opts.agent - whether the Compose file sets up the host agent
118
+ */
119
+ export function selfHostedBootstrapScript(opts: { readonly agent: boolean }): string {
120
+ const roles: readonly (AppRole | typeof AGENT_DB_ROLE)[] = [
121
+ ...APP_ROLES,
122
+ ...(opts.agent ? [AGENT_DB_ROLE as typeof AGENT_DB_ROLE] : []),
123
+ ];
124
+ const variables = roles.map((role) => ` -v ${role}_pw="$${PASSWORD_ENV[role]}" \\`).join("\n");
125
+ const checks = roles
126
+ .map((role) => `: "\${${PASSWORD_ENV[role]}:?must be set in environment}"`)
127
+ .join("\n");
128
+ return `#!/bin/sh
129
+ # SPDX-License-Identifier: MPL-2.0
130
+ # Generated by cms-provision — do not edit. Runs once, on the Postgres
131
+ # container's first start: creates the Caelo roles and databases. Every
132
+ # password comes from the container's environment (docker-compose.yml).
133
+ set -e
134
+ ${checks}
135
+ psql --username "$POSTGRES_USER" --dbname postgres -q \\
136
+ ${variables}
137
+ -f - <<'CAELO_SQL'
138
+ ${convergenceSql((role) => `:'${role}_pw'`, opts.agent)}CAELO_SQL
139
+ `;
140
+ }
141
+
142
+ /** Runs a psql script as the superuser; resolves to psql's exit code + stderr. */
143
+ export type SuperuserPsql = (script: string) => Promise<{ exitCode: number; stderr: string }>;
144
+
145
+ export type ConvergeOutcome =
146
+ | { readonly ok: true }
147
+ /** `reachable: false` — the database is not running, nothing was attempted. */
148
+ | { readonly ok: false; readonly reachable: boolean; readonly error: string };
149
+
150
+ /**
151
+ * Apply {@link roleConvergenceScript}. `probe` says whether the database is
152
+ * up; when it is not, nothing is attempted and the caller decides whether
153
+ * that is acceptable (it is not while the config's passwords were never
154
+ * applied).
155
+ */
156
+ export async function convergeSelfHostedRoles(
157
+ passwords: RolePasswords,
158
+ deps: {
159
+ readonly probe: () => Promise<boolean>;
160
+ readonly psql: SuperuserPsql;
161
+ /** The host agent's password, when the agent is set up (#696). */
162
+ readonly agentPassword?: string;
163
+ },
164
+ ): Promise<ConvergeOutcome> {
165
+ if (!(await deps.probe())) {
166
+ return { ok: false, reachable: false, error: "the Postgres container is not running" };
167
+ }
168
+ const r = await deps.psql(roleConvergenceScript(passwords, deps.agentPassword));
169
+ if (r.exitCode === 0) return { ok: true };
170
+ return { ok: false, reachable: true, error: r.stderr.trim() || `psql exited ${r.exitCode}` };
171
+ }
172
+
173
+ /** What `cms-provision up` does with a convergence outcome. */
174
+ export type UpDecision =
175
+ | { readonly kind: "proceed"; readonly markConverged: boolean; readonly warning?: string }
176
+ | { readonly kind: "abort"; readonly error: string };
177
+
178
+ /**
179
+ * Whether `up` may write Compose URLs with the configured passwords. Never
180
+ * while they are not known to be applied: an install whose config just got
181
+ * its role passwords (`rolesConverged: false`) must converge first.
182
+ */
183
+ export function decideUp(rolesConverged: boolean, outcome: ConvergeOutcome): UpDecision {
184
+ if (outcome.ok) return { kind: "proceed", markConverged: !rolesConverged };
185
+ if (outcome.reachable) {
186
+ return {
187
+ kind: "abort",
188
+ error: `could not bring the database roles in line with .caelo/config.json: ${outcome.error}. Nothing was changed; fix the cause and re-run \`cms-provision up\`.`,
189
+ };
190
+ }
191
+ if (!rolesConverged) {
192
+ return {
193
+ kind: "abort",
194
+ error:
195
+ "the database roles must be updated before the services switch to their new passwords, but the Postgres container is not running. Start it (`docker compose -f .caelo/docker-compose.yml up -d postgres`) and re-run `cms-provision up`. Nothing was changed.",
196
+ };
197
+ }
198
+ return {
199
+ kind: "proceed",
200
+ markConverged: false,
201
+ warning: "the Postgres container is not running; the database roles were not re-checked",
202
+ };
203
+ }