@caelo-cms/provisioning 0.10.47 → 0.10.49

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 (321) hide show
  1. package/caddy/Caddyfile.production +1 -1
  2. package/caddy/Caddyfile.staging +1 -1
  3. package/dist/agent/apply.d.ts +45 -0
  4. package/dist/agent/apply.d.ts.map +1 -0
  5. package/dist/agent/apply.js +83 -0
  6. package/dist/agent/apply.js.map +1 -0
  7. package/dist/agent/cli-provenance.d.ts +42 -0
  8. package/dist/agent/cli-provenance.d.ts.map +1 -0
  9. package/dist/agent/cli-provenance.js +138 -0
  10. package/dist/agent/cli-provenance.js.map +1 -0
  11. package/dist/agent/command.d.ts +79 -0
  12. package/dist/agent/command.d.ts.map +1 -0
  13. package/dist/agent/command.js +454 -0
  14. package/dist/agent/command.js.map +1 -0
  15. package/dist/agent/config.d.ts +30 -0
  16. package/dist/agent/config.d.ts.map +1 -0
  17. package/dist/agent/config.js +60 -0
  18. package/dist/agent/config.js.map +1 -0
  19. package/dist/agent/lock.d.ts +13 -0
  20. package/dist/agent/lock.d.ts.map +1 -0
  21. package/dist/agent/lock.js +100 -0
  22. package/dist/agent/lock.js.map +1 -0
  23. package/dist/agent/queue.d.ts +35 -0
  24. package/dist/agent/queue.d.ts.map +1 -0
  25. package/dist/agent/queue.js +50 -0
  26. package/dist/agent/queue.js.map +1 -0
  27. package/dist/agent/run.d.ts +97 -0
  28. package/dist/agent/run.d.ts.map +1 -0
  29. package/dist/agent/run.js +198 -0
  30. package/dist/agent/run.js.map +1 -0
  31. package/dist/agent/systemd.d.ts +55 -0
  32. package/dist/agent/systemd.d.ts.map +1 -0
  33. package/dist/agent/systemd.js +171 -0
  34. package/dist/agent/systemd.js.map +1 -0
  35. package/dist/agent/units.d.ts +44 -0
  36. package/dist/agent/units.d.ts.map +1 -0
  37. package/dist/agent/units.js +127 -0
  38. package/dist/agent/units.js.map +1 -0
  39. package/dist/agent-db.js +18402 -0
  40. package/dist/cli.js +119 -16
  41. package/dist/cli.js.map +1 -1
  42. package/dist/cloudsql-iam-user.d.ts +39 -0
  43. package/dist/cloudsql-iam-user.d.ts.map +1 -0
  44. package/dist/cloudsql-iam-user.js +119 -0
  45. package/dist/cloudsql-iam-user.js.map +1 -0
  46. package/dist/compose.d.ts +16 -0
  47. package/dist/compose.d.ts.map +1 -1
  48. package/dist/compose.js +26 -1
  49. package/dist/compose.js.map +1 -1
  50. package/dist/dns/index.d.ts +2 -0
  51. package/dist/dns/index.d.ts.map +1 -1
  52. package/dist/dns/index.js +3 -2
  53. package/dist/dns/index.js.map +1 -1
  54. package/dist/dns/manual.d.ts +5 -0
  55. package/dist/dns/manual.d.ts.map +1 -1
  56. package/dist/dns/manual.js +28 -12
  57. package/dist/dns/manual.js.map +1 -1
  58. package/dist/gcloud.d.ts +2 -0
  59. package/dist/gcloud.d.ts.map +1 -1
  60. package/dist/gcloud.js +1 -1
  61. package/dist/gcloud.js.map +1 -1
  62. package/dist/gcp-names.d.ts +15 -0
  63. package/dist/gcp-names.d.ts.map +1 -1
  64. package/dist/gcp-names.js +23 -0
  65. package/dist/gcp-names.js.map +1 -1
  66. package/dist/index.d.ts +6 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +4 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/infra-contract.d.ts +1 -1
  71. package/dist/infra-contract.js +7 -1
  72. package/dist/infra-contract.js.map +1 -1
  73. package/dist/install-job-runner.d.ts +13 -0
  74. package/dist/install-job-runner.d.ts.map +1 -0
  75. package/dist/install-job-runner.js +155 -0
  76. package/dist/install-job-runner.js.map +1 -0
  77. package/dist/install-job.d.ts +210 -0
  78. package/dist/install-job.d.ts.map +1 -0
  79. package/dist/install-job.js +449 -0
  80. package/dist/install-job.js.map +1 -0
  81. package/dist/install-state.d.ts +10 -1
  82. package/dist/install-state.d.ts.map +1 -1
  83. package/dist/install-state.js +10 -2
  84. package/dist/install-state.js.map +1 -1
  85. package/dist/lifecycle.d.ts.map +1 -1
  86. package/dist/lifecycle.js +19 -4
  87. package/dist/lifecycle.js.map +1 -1
  88. package/dist/migration-runner.d.ts +5 -0
  89. package/dist/migration-runner.d.ts.map +1 -1
  90. package/dist/migration-runner.js +1 -1
  91. package/dist/migration-runner.js.map +1 -1
  92. package/dist/operator-access.d.ts +14 -8
  93. package/dist/operator-access.d.ts.map +1 -1
  94. package/dist/operator-access.js +16 -102
  95. package/dist/operator-access.js.map +1 -1
  96. package/dist/release-manifest.d.ts +2 -41
  97. package/dist/release-manifest.d.ts.map +1 -1
  98. package/dist/release-manifest.js +18 -121
  99. package/dist/release-manifest.js.map +1 -1
  100. package/dist/run-mode.d.ts +27 -0
  101. package/dist/run-mode.d.ts.map +1 -0
  102. package/dist/run-mode.js +22 -0
  103. package/dist/run-mode.js.map +1 -0
  104. package/dist/runtime-secrets.d.ts +19 -0
  105. package/dist/runtime-secrets.d.ts.map +1 -1
  106. package/dist/runtime-secrets.js +44 -0
  107. package/dist/runtime-secrets.js.map +1 -1
  108. package/dist/self-hosted-roles.d.ts +22 -1
  109. package/dist/self-hosted-roles.d.ts.map +1 -1
  110. package/dist/self-hosted-roles.js +81 -12
  111. package/dist/self-hosted-roles.js.map +1 -1
  112. package/dist/self-hosted-upgrade.d.ts +17 -0
  113. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  114. package/dist/self-hosted-upgrade.js +16 -1
  115. package/dist/self-hosted-upgrade.js.map +1 -1
  116. package/dist/stack-contract.d.ts +6 -0
  117. package/dist/stack-contract.d.ts.map +1 -1
  118. package/dist/stack-contract.js +10 -1
  119. package/dist/stack-contract.js.map +1 -1
  120. package/dist/upgrade/converge.d.ts +6 -0
  121. package/dist/upgrade/converge.d.ts.map +1 -1
  122. package/dist/upgrade/converge.js +59 -2
  123. package/dist/upgrade/converge.js.map +1 -1
  124. package/dist/upgrade/gcp-lookup.d.ts +6 -0
  125. package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
  126. package/dist/upgrade/gcp-lookup.js +25 -0
  127. package/dist/upgrade/gcp-lookup.js.map +1 -1
  128. package/dist/upgrade/release-images.d.ts +26 -2
  129. package/dist/upgrade/release-images.d.ts.map +1 -1
  130. package/dist/upgrade/release-images.js +35 -2
  131. package/dist/upgrade/release-images.js.map +1 -1
  132. package/dist/upgrade/upgrade.d.ts +5 -0
  133. package/dist/upgrade/upgrade.d.ts.map +1 -1
  134. package/dist/upgrade/upgrade.js +28 -1
  135. package/dist/upgrade/upgrade.js.map +1 -1
  136. package/dist/upgrader-assess.d.ts +85 -0
  137. package/dist/upgrader-assess.d.ts.map +1 -0
  138. package/dist/upgrader-assess.js +122 -0
  139. package/dist/upgrader-assess.js.map +1 -0
  140. package/dist/upgrader-gcp.d.ts +17 -0
  141. package/dist/upgrader-gcp.d.ts.map +1 -0
  142. package/dist/upgrader-gcp.js +87 -0
  143. package/dist/upgrader-gcp.js.map +1 -0
  144. package/dist/upgrader-job.d.ts +133 -0
  145. package/dist/upgrader-job.d.ts.map +1 -0
  146. package/dist/upgrader-job.js +259 -0
  147. package/dist/upgrader-job.js.map +1 -0
  148. package/dist/upgrader-reporter.d.ts +35 -0
  149. package/dist/upgrader-reporter.d.ts.map +1 -0
  150. package/dist/upgrader-reporter.js +79 -0
  151. package/dist/upgrader-reporter.js.map +1 -0
  152. package/dist/upgrader-setup.d.ts +59 -0
  153. package/dist/upgrader-setup.d.ts.map +1 -0
  154. package/dist/upgrader-setup.js +308 -0
  155. package/dist/upgrader-setup.js.map +1 -0
  156. package/dist/wizard.d.ts +6 -0
  157. package/dist/wizard.d.ts.map +1 -1
  158. package/dist/wizard.js +32 -4
  159. package/dist/wizard.js.map +1 -1
  160. package/dist/wizards/gcp-firebase.d.ts +3 -0
  161. package/dist/wizards/gcp-firebase.d.ts.map +1 -1
  162. package/dist/wizards/gcp-firebase.js +1 -0
  163. package/dist/wizards/gcp-firebase.js.map +1 -1
  164. package/dist/wizards/gcp-install-job.d.ts +74 -0
  165. package/dist/wizards/gcp-install-job.d.ts.map +1 -0
  166. package/dist/wizards/gcp-install-job.js +398 -0
  167. package/dist/wizards/gcp-install-job.js.map +1 -0
  168. package/dist/wizards/gcp-prompts.d.ts +3 -0
  169. package/dist/wizards/gcp-prompts.d.ts.map +1 -0
  170. package/dist/wizards/gcp-prompts.js +25 -0
  171. package/dist/wizards/gcp-prompts.js.map +1 -0
  172. package/dist/wizards/gcp-pulumi.d.ts +4 -2
  173. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  174. package/dist/wizards/gcp-pulumi.js +1 -1
  175. package/dist/wizards/gcp-pulumi.js.map +1 -1
  176. package/dist/wizards/gcp.d.ts +40 -0
  177. package/dist/wizards/gcp.d.ts.map +1 -1
  178. package/dist/wizards/gcp.js +223 -59
  179. package/dist/wizards/gcp.js.map +1 -1
  180. package/package.json +6 -4
  181. package/src/adapter.ts +97 -0
  182. package/src/adc-impersonation.ts +176 -0
  183. package/src/admin-domain.test.ts +254 -0
  184. package/src/admin-domain.ts +245 -0
  185. package/src/admin-memory.test.ts +151 -0
  186. package/src/agent/apply.test.ts +185 -0
  187. package/src/agent/apply.ts +124 -0
  188. package/src/agent/cli-provenance.test.ts +159 -0
  189. package/src/agent/cli-provenance.ts +191 -0
  190. package/src/agent/command.test.ts +77 -0
  191. package/src/agent/command.ts +564 -0
  192. package/src/agent/config.ts +86 -0
  193. package/src/agent/lock-config.test.ts +97 -0
  194. package/src/agent/lock.ts +110 -0
  195. package/src/agent/queue.test.ts +10 -0
  196. package/src/agent/queue.ts +91 -0
  197. package/src/agent/run.test.ts +437 -0
  198. package/src/agent/run.ts +295 -0
  199. package/src/agent/systemd.ts +209 -0
  200. package/src/agent/units.test.ts +223 -0
  201. package/src/agent/units.ts +163 -0
  202. package/src/azure-front-door.test.ts +119 -0
  203. package/src/azure-front-door.ts +56 -0
  204. package/src/bootstrap-token.ts +20 -0
  205. package/src/caddy.ts +112 -0
  206. package/src/cdn-copy.ts +91 -0
  207. package/src/cert-wait.test.ts +194 -0
  208. package/src/cert-wait.ts +163 -0
  209. package/src/checkpoint-secrets.test.ts +380 -0
  210. package/src/checkpoint-secrets.ts +283 -0
  211. package/src/cli-routing.test.ts +45 -0
  212. package/src/cli-routing.ts +43 -0
  213. package/src/cli.ts +1491 -0
  214. package/src/cloudsql-iam-user.ts +147 -0
  215. package/src/compose.ts +334 -0
  216. package/src/default-firewall.test.ts +411 -0
  217. package/src/default-firewall.ts +342 -0
  218. package/src/deployed-release.test.ts +231 -0
  219. package/src/deployed-release.ts +220 -0
  220. package/src/dns/cloudflare.ts +182 -0
  221. package/src/dns/groups.test.ts +76 -0
  222. package/src/dns/groups.ts +86 -0
  223. package/src/dns/index.ts +56 -0
  224. package/src/dns/manual.ts +130 -0
  225. package/src/dns/types.ts +27 -0
  226. package/src/gateway-credentials.test.ts +180 -0
  227. package/src/gateway-credentials.ts +144 -0
  228. package/src/gcloud-components.test.ts +76 -0
  229. package/src/gcloud-components.ts +85 -0
  230. package/src/gcloud-retry.ts +41 -0
  231. package/src/gcloud-runner.test.ts +61 -0
  232. package/src/gcloud.test.ts +99 -0
  233. package/src/gcloud.ts +322 -0
  234. package/src/gcp-names.ts +135 -0
  235. package/src/index.test.ts +332 -0
  236. package/src/index.ts +114 -0
  237. package/src/infra-contract.ts +32 -0
  238. package/src/install-job-runner.ts +166 -0
  239. package/src/install-job.test.ts +566 -0
  240. package/src/install-job.ts +542 -0
  241. package/src/install-state.test.ts +239 -0
  242. package/src/install-state.ts +583 -0
  243. package/src/internal-token.ts +35 -0
  244. package/src/lifecycle.ts +900 -0
  245. package/src/mcp-iap-stacks.test.ts +58 -0
  246. package/src/mcp-iap.test.ts +149 -0
  247. package/src/mcp-iap.ts +131 -0
  248. package/src/media-volume.test.ts +296 -0
  249. package/src/migration-runner.ts +365 -0
  250. package/src/operator-access-stacks.test.ts +55 -0
  251. package/src/operator-access.test.ts +332 -0
  252. package/src/operator-access.ts +554 -0
  253. package/src/ops-backend.test.ts +320 -0
  254. package/src/ops-backend.ts +414 -0
  255. package/src/provisioner-identity.test.ts +482 -0
  256. package/src/provisioner-identity.ts +407 -0
  257. package/src/redirects-emit.ts +175 -0
  258. package/src/region-data.ts +428 -0
  259. package/src/regions.test.ts +277 -0
  260. package/src/regions.ts +370 -0
  261. package/src/release-contract.ts +57 -0
  262. package/src/release-manifest.test.ts +300 -0
  263. package/src/release-manifest.ts +310 -0
  264. package/src/run-mode.ts +40 -0
  265. package/src/runtime-secrets.test.ts +627 -0
  266. package/src/runtime-secrets.ts +754 -0
  267. package/src/self-command.test.ts +14 -0
  268. package/src/self-command.ts +21 -0
  269. package/src/self-hosted-compose.test.ts +177 -0
  270. package/src/self-hosted-release.ts +240 -0
  271. package/src/self-hosted-roles.integration.test.ts +161 -0
  272. package/src/self-hosted-roles.test.ts +124 -0
  273. package/src/self-hosted-roles.ts +203 -0
  274. package/src/self-hosted-upgrade.test.ts +394 -0
  275. package/src/self-hosted-upgrade.ts +548 -0
  276. package/src/stack-contract-parity.test.ts +199 -0
  277. package/src/stack-contract.test.ts +391 -0
  278. package/src/stack-contract.ts +888 -0
  279. package/src/stack-converge-secrets.test.ts +421 -0
  280. package/src/stack-converge.test.ts +833 -0
  281. package/src/stack-converge.ts +909 -0
  282. package/src/staging-admin.test.ts +246 -0
  283. package/src/staging-admin.ts +324 -0
  284. package/src/state-attach.test.ts +211 -0
  285. package/src/state-attach.ts +132 -0
  286. package/src/state-backend.test.ts +145 -0
  287. package/src/state-backend.ts +262 -0
  288. package/src/state-command.ts +293 -0
  289. package/src/state-migrate.test.ts +479 -0
  290. package/src/state-migrate.ts +434 -0
  291. package/src/state-pulumi.ts +289 -0
  292. package/src/state-store.test.ts +212 -0
  293. package/src/state-store.ts +243 -0
  294. package/src/upgrade/app-roll.ts +486 -0
  295. package/src/upgrade/backup.ts +109 -0
  296. package/src/upgrade/converge.ts +437 -0
  297. package/src/upgrade/gcp-lookup.ts +167 -0
  298. package/src/upgrade/health.test.ts +279 -0
  299. package/src/upgrade/health.ts +305 -0
  300. package/src/upgrade/release-images.test.ts +119 -0
  301. package/src/upgrade/release-images.ts +393 -0
  302. package/src/upgrade/reporter.ts +46 -0
  303. package/src/upgrade/upgrade.test.ts +626 -0
  304. package/src/upgrade/upgrade.ts +249 -0
  305. package/src/upgrader-assess.test.ts +306 -0
  306. package/src/upgrader-assess.ts +250 -0
  307. package/src/upgrader-gcp.ts +123 -0
  308. package/src/upgrader-job.test.ts +411 -0
  309. package/src/upgrader-job.ts +502 -0
  310. package/src/upgrader-reporter.test.ts +46 -0
  311. package/src/upgrader-reporter.ts +105 -0
  312. package/src/upgrader-setup.test.ts +169 -0
  313. package/src/upgrader-setup.ts +355 -0
  314. package/src/wizard.ts +425 -0
  315. package/src/wizards/gcp-cost.ts +188 -0
  316. package/src/wizards/gcp-firebase.ts +72 -0
  317. package/src/wizards/gcp-install-job.ts +506 -0
  318. package/src/wizards/gcp-prompts.ts +26 -0
  319. package/src/wizards/gcp-pulumi.test.ts +246 -0
  320. package/src/wizards/gcp-pulumi.ts +516 -0
  321. package/src/wizards/gcp.ts +1935 -0
@@ -0,0 +1,254 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import { enableAdminDomain } from "./admin-domain.js";
5
+ import type { GcloudResult } from "./gcloud.js";
6
+
7
+ const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
8
+ const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
9
+
10
+ /** Fake gcloud; the `beta` component is installed unless a test answers `components list`. */
11
+ function fakeGcloud(overrides: Record<string, GcloudResult[]>) {
12
+ const answers: Record<string, GcloudResult[]> = {
13
+ "components list": [ok("beta\n")],
14
+ ...overrides,
15
+ };
16
+ const calls: string[][] = [];
17
+ const run = async (args: string[]) => {
18
+ calls.push(args);
19
+ const key = Object.keys(answers).find((k) => args.join(" ").startsWith(k));
20
+ return (key && answers[key]?.shift()) || ok();
21
+ };
22
+ return { run, calls };
23
+ }
24
+
25
+ const MAPPING = JSON.stringify({
26
+ status: { resourceRecords: [{ name: "admin", type: "CNAME", rrdata: "ghs.googlehosted.com." }] },
27
+ });
28
+ const base = { projectId: "p", region: "europe-west1", domain: "example.com", log: () => {} };
29
+
30
+ describe("enableAdminDomain", () => {
31
+ it("creates the mapping as the operator once the domain is verified, and prints the record", async () => {
32
+ const { run, calls } = fakeGcloud({
33
+ "run services list": [ok("caelo-production-admin-abc\n")],
34
+ "beta run domain-mappings describe": [fail("NOT_FOUND"), ok(MAPPING)],
35
+ "domains list-user-verified": [ok("example.com\n")],
36
+ });
37
+ const r = await enableAdminDomain({ ...base, run });
38
+ expect(r).toEqual({
39
+ status: "mapped",
40
+ hostname: "admin.example.com",
41
+ created: true,
42
+ records: [{ name: "admin.example.com", type: "CNAME", value: "ghs.googlehosted.com." }],
43
+ });
44
+ expect(calls).toContainEqual([
45
+ "beta",
46
+ "run",
47
+ "domain-mappings",
48
+ "create",
49
+ "--service=caelo-production-admin-abc",
50
+ "--domain=admin.example.com",
51
+ "--region=europe-west1",
52
+ "--project=p",
53
+ "--quiet",
54
+ ]);
55
+ });
56
+
57
+ it("is idempotent: an existing mapping is reported, not recreated", async () => {
58
+ const { run, calls } = fakeGcloud({
59
+ "run services list": [ok("caelo-production-admin-abc\n")],
60
+ "beta run domain-mappings describe": [ok(MAPPING)],
61
+ });
62
+ const r = await enableAdminDomain({ ...base, run });
63
+ expect(r.status === "mapped" && !r.created).toBe(true);
64
+ expect(calls.some((c) => c.includes("create"))).toBe(false);
65
+ });
66
+
67
+ it("starts Search Console verification and stops when the operator has not verified the domain", async () => {
68
+ const { run, calls } = fakeGcloud({
69
+ "run services list": [ok("caelo-production-admin-abc\n")],
70
+ "beta run domain-mappings describe": [fail("NOT_FOUND")],
71
+ "domains list-user-verified": [ok("other.org\n")],
72
+ });
73
+ const r = await enableAdminDomain({ ...base, run });
74
+ expect(r).toEqual({
75
+ status: "needs-verification",
76
+ hostname: "admin.example.com",
77
+ verifyDomain: "example.com",
78
+ });
79
+ expect(calls).toContainEqual(["domains", "verify", "example.com"]);
80
+ expect(calls.some((c) => c.includes("create"))).toBe(false);
81
+ });
82
+
83
+ it("refuses to pick when more than one admin service matches", async () => {
84
+ const { run, calls } = fakeGcloud({
85
+ "run services list": [ok("caelo-production-admin-abc\ncaelo-production-admin-old\n")],
86
+ });
87
+ const r = await enableAdminDomain({ ...base, run });
88
+ expect(r.status === "failed" && r.error).toContain("exactly one");
89
+ expect(calls.some((c) => c.includes("create"))).toBe(false);
90
+ });
91
+
92
+ it("fails (not needs-verification) when gcloud cannot list or start verification", async () => {
93
+ const listFails = fakeGcloud({
94
+ "run services list": [ok("caelo-production-admin-abc\n")],
95
+ "beta run domain-mappings describe": [fail("NOT_FOUND")],
96
+ "domains list-user-verified": [fail("Reauthentication required")],
97
+ });
98
+ expect(await enableAdminDomain({ ...base, run: listFails.run })).toEqual({
99
+ status: "failed",
100
+ error: "list verified domains: Reauthentication required",
101
+ });
102
+
103
+ const verifyFails = fakeGcloud({
104
+ "run services list": [ok("caelo-production-admin-abc\n")],
105
+ "beta run domain-mappings describe": [fail("NOT_FOUND")],
106
+ "domains list-user-verified": [ok("other.org\n")],
107
+ "domains verify": [fail("could not open a browser")],
108
+ });
109
+ expect(await enableAdminDomain({ ...base, run: verifyFails.run })).toEqual({
110
+ status: "failed",
111
+ error: "start domain verification for example.com: could not open a browser",
112
+ });
113
+ });
114
+
115
+ it("fails loudly when the admin service cannot be found or the create is refused", async () => {
116
+ const missing = fakeGcloud({ "run services list": [ok("")] });
117
+ expect((await enableAdminDomain({ ...base, run: missing.run })).status).toBe("failed");
118
+
119
+ const refused = fakeGcloud({
120
+ "run services list": [ok("caelo-production-admin-abc\n")],
121
+ "beta run domain-mappings describe": [fail("NOT_FOUND")],
122
+ "domains list-user-verified": [ok("admin.example.com\n")],
123
+ "beta run domain-mappings create": [
124
+ fail("Caller is not authorized to administer the domain"),
125
+ ],
126
+ });
127
+ expect(await enableAdminDomain({ ...base, run: refused.run })).toEqual({
128
+ status: "failed",
129
+ error: "create domain mapping: Caller is not authorized to administer the domain",
130
+ });
131
+ });
132
+
133
+ it("installs the missing beta component before the first beta command (regression: hidden gcloud prompt hung the CLI)", async () => {
134
+ const logs: string[] = [];
135
+ const { run, calls } = fakeGcloud({
136
+ "components list": [ok("")],
137
+ "run services list": [ok("caelo-production-admin-abc\n")],
138
+ "beta run domain-mappings describe": [ok(MAPPING)],
139
+ });
140
+ const r = await enableAdminDomain({ ...base, run, log: (m) => logs.push(m) });
141
+ expect(r.status).toBe("mapped");
142
+ const install = calls.findIndex((c) => c.join(" ") === "components install beta --quiet");
143
+ const firstBeta = calls.findIndex((c) => c[0] === "beta");
144
+ expect(install).toBeGreaterThanOrEqual(0);
145
+ expect(install).toBeLessThan(firstBeta);
146
+ expect(logs.join("\n")).toContain('Installing the gcloud "beta" component');
147
+ });
148
+
149
+ it("fails with the package-manager command when gcloud cannot install the beta component", async () => {
150
+ const { run, calls } = fakeGcloud({
151
+ "components list": [ok("")],
152
+ "run services list": [ok("caelo-production-admin-abc\n")],
153
+ "components install": [
154
+ fail(
155
+ "ERROR: (gcloud.components.install)\nYou cannot perform this action because the Google Cloud CLI component manager \nis disabled for this installation. You can run the following command \nto achieve the same result for this installation: \n\nsudo apt-get install google-cloud-cli",
156
+ ),
157
+ ],
158
+ });
159
+ const r = await enableAdminDomain({ ...base, run });
160
+ expect(r.status).toBe("failed");
161
+ expect(r.status === "failed" && r.error).toContain("sudo apt-get install google-cloud-cli");
162
+ expect(calls.some((c) => c[0] === "beta")).toBe(false);
163
+ });
164
+ });
165
+
166
+ describe("enableStagingDomain (#650)", () => {
167
+ const STAGING_MAPPING = JSON.stringify({
168
+ status: {
169
+ resourceRecords: [{ name: "staging", type: "CNAME", rrdata: "ghs.googlehosted.com." }],
170
+ },
171
+ });
172
+
173
+ it("opens the gateway's ingress, maps staging.<domain> to the gateway and prints the record", async () => {
174
+ const { run, calls } = fakeGcloud({
175
+ "run services list": [ok("caelo-production-gateway-xyz\n")],
176
+ "run services describe": [ok("internal-and-cloud-load-balancing\n")],
177
+ "beta run domain-mappings describe": [fail("NOT_FOUND"), ok(STAGING_MAPPING)],
178
+ "domains list-user-verified": [ok("example.com\n")],
179
+ });
180
+ const { enableStagingDomain } = await import("./admin-domain.js");
181
+ const r = await enableStagingDomain({ ...base, stagingHost: "staging.example.com", run });
182
+ expect(r).toEqual({
183
+ status: "mapped",
184
+ hostname: "staging.example.com",
185
+ created: true,
186
+ records: [{ name: "staging.example.com", type: "CNAME", value: "ghs.googlehosted.com." }],
187
+ });
188
+ expect(calls).toContainEqual([
189
+ "run",
190
+ "services",
191
+ "update",
192
+ "caelo-production-gateway-xyz",
193
+ "--region=europe-west1",
194
+ "--project=p",
195
+ "--ingress=all",
196
+ "--quiet",
197
+ ]);
198
+ expect(calls.find((c) => c.includes("create"))).toContain(
199
+ "--service=caelo-production-gateway-xyz",
200
+ );
201
+ expect(calls[0]).toContain("--filter=metadata.name~^caelo-production-gateway");
202
+ });
203
+
204
+ it("is idempotent: ingress already open and mapping present ⇒ no update, no create", async () => {
205
+ const { run, calls } = fakeGcloud({
206
+ "run services list": [ok("caelo-production-gateway-xyz\n")],
207
+ "run services describe": [ok("all\n")],
208
+ "beta run domain-mappings describe": [ok(STAGING_MAPPING)],
209
+ });
210
+ const { enableStagingDomain } = await import("./admin-domain.js");
211
+ const r = await enableStagingDomain({ ...base, stagingHost: "staging.example.com", run });
212
+ expect(r.status === "mapped" && !r.created).toBe(true);
213
+ expect(calls.some((c) => c.includes("update") || c.includes("create"))).toBe(false);
214
+ });
215
+
216
+ it("maps a host the operator chose on another domain, verifying that host itself", async () => {
217
+ const { run, calls } = fakeGcloud({
218
+ "run services list": [ok("caelo-production-gateway-xyz\n")],
219
+ "run services describe": [ok("all\n")],
220
+ "beta run domain-mappings describe": [fail("NOT_FOUND")],
221
+ "domains list-user-verified": [ok("example.com\n")],
222
+ });
223
+ const { enableStagingDomain } = await import("./admin-domain.js");
224
+ const r = await enableStagingDomain({ ...base, stagingHost: "preview.other.de", run });
225
+ expect(r).toEqual({
226
+ status: "needs-verification",
227
+ hostname: "preview.other.de",
228
+ verifyDomain: "preview.other.de",
229
+ });
230
+ expect(calls).toContainEqual(["domains", "verify", "preview.other.de"]);
231
+ });
232
+
233
+ it("fails closed without exactly one gateway service", async () => {
234
+ const { run } = fakeGcloud({ "run services list": [ok("a\nb\n")] });
235
+ const { enableStagingDomain } = await import("./admin-domain.js");
236
+ const r = await enableStagingDomain({ ...base, stagingHost: "staging.example.com", run });
237
+ expect(r.status).toBe("failed");
238
+ });
239
+
240
+ it("installs the missing beta component before its first beta command", async () => {
241
+ const { run, calls } = fakeGcloud({
242
+ "components list": [ok("")],
243
+ "run services list": [ok("caelo-production-gateway-xyz\n")],
244
+ "run services describe": [ok("all\n")],
245
+ "beta run domain-mappings describe": [ok(STAGING_MAPPING)],
246
+ });
247
+ const { enableStagingDomain } = await import("./admin-domain.js");
248
+ const r = await enableStagingDomain({ ...base, stagingHost: "staging.example.com", run });
249
+ expect(r.status).toBe("mapped");
250
+ const install = calls.findIndex((c) => c.join(" ") === "components install beta --quiet");
251
+ expect(install).toBeGreaterThanOrEqual(0);
252
+ expect(install).toBeLessThan(calls.findIndex((c) => c[0] === "beta"));
253
+ });
254
+ });
@@ -0,0 +1,245 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * `cms-provision admin-domain enable` — serve a gcp-firebase admin at
5
+ * `admin.<domain>` instead of its `*.run.app` URL — and, the same way,
6
+ * `cms-provision staging-domain enable` (#650): serve the protected staging
7
+ * site at `staging.<domain>` from the gateway, which gates and proxies it.
8
+ *
9
+ * The gcp-firebase stack only creates that Cloud Run domain mapping behind
10
+ * `provisionAdminDomain=true`, off by default: Google refuses a mapping
11
+ * unless the CALLER is a verified owner of the domain in Search Console, and
12
+ * the stack runs as the provisioner service account, which never is. So the
13
+ * mapping is created here, as the operator's own gcloud identity — the one
14
+ * that verified (or now verifies) the domain:
15
+ *
16
+ * 1. find the admin Cloud Run service (`caelo-production-admin*`);
17
+ * 2. if the mapping exists already, just report its DNS records;
18
+ * 3. check `gcloud domains list-user-verified`; if neither the apex nor
19
+ * `admin.<domain>` is verified, start `gcloud domains verify <apex>`
20
+ * (opens Search Console) and stop — verifying means adding a TXT record,
21
+ * the one step only the domain's owner can take;
22
+ * 4. create the mapping and print the DNS records to add.
23
+ *
24
+ * The mapping is then owned by this command, not by Pulumi: leave
25
+ * `provisionAdminDomain` false, or the next `pulumi up` tries to create a
26
+ * second one and fails with "already exists".
27
+ *
28
+ * `gcp` installs need none of this — the load balancer serves admin.<domain>
29
+ * from the start.
30
+ */
31
+
32
+ import { gcloud as defaultGcloud, type GcloudResult } from "./gcloud.js";
33
+ import { ensureGcloudComponent } from "./gcloud-components.js";
34
+
35
+ type Run = (args: string[]) => Promise<GcloudResult>;
36
+
37
+ export interface DnsRecordToAdd {
38
+ readonly name: string;
39
+ readonly type: string;
40
+ readonly value: string;
41
+ }
42
+
43
+ export type AdminDomainResult =
44
+ | { status: "mapped"; hostname: string; created: boolean; records: DnsRecordToAdd[] }
45
+ | { status: "needs-verification"; hostname: string; verifyDomain: string }
46
+ | { status: "failed"; error: string };
47
+
48
+ /** What `staging-domain enable` reports; same shape as the admin's. */
49
+ export type StagingDomainResult = AdminDomainResult;
50
+
51
+ interface DomainMappingJson {
52
+ status?: { resourceRecords?: { name?: string; type?: string; rrdata?: string }[] };
53
+ }
54
+
55
+ function recordsOf(json: string, hostname: string): DnsRecordToAdd[] {
56
+ const parsed = JSON.parse(json) as DomainMappingJson;
57
+ return (parsed.status?.resourceRecords ?? []).map((r) => ({
58
+ name: r.name ? `${r.name}.${hostname.split(".").slice(1).join(".")}` : hostname,
59
+ type: r.type ?? "CNAME",
60
+ value: r.rrdata ?? "",
61
+ }));
62
+ }
63
+
64
+ interface DomainOpts {
65
+ projectId: string;
66
+ region: string;
67
+ domain: string;
68
+ run?: Run;
69
+ /** Progress lines (the one-time `beta` component install); defaults to stdout. */
70
+ log?: (message: string) => void;
71
+ }
72
+
73
+ function logOf(opts: DomainOpts): (message: string) => void {
74
+ return opts.log ?? ((message: string) => console.log(message));
75
+ }
76
+
77
+ /** The one deployed Cloud Run service of `slug` (Pulumi-suffixed name); also used by staging-admin.ts. */
78
+ export async function findService(
79
+ run: Run,
80
+ scope: string[],
81
+ slug: "admin" | "gateway",
82
+ ): Promise<{ ok: true; service: string } | { ok: false; error: string }> {
83
+ const prefix = `caelo-production-${slug}`;
84
+ const services = await run([
85
+ "run",
86
+ "services",
87
+ "list",
88
+ ...scope,
89
+ `--filter=metadata.name~^${prefix}`,
90
+ "--format=value(metadata.name)",
91
+ ]);
92
+ if (!services.ok) {
93
+ return { ok: false, error: `list Cloud Run services: ${services.stderr.trim()}` };
94
+ }
95
+ const matches = services.stdout
96
+ .split("\n")
97
+ .map((s) => s.trim())
98
+ .filter(Boolean);
99
+ // Fail closed like the other service lookups: with a leftover service from
100
+ // a failed deploy, picking one would bind the domain to the wrong revision.
101
+ const service = matches.length === 1 ? matches[0] : undefined;
102
+ if (!service) {
103
+ return {
104
+ ok: false,
105
+ error:
106
+ matches.length === 0
107
+ ? `${slug} Cloud Run service not found: no ${prefix}* service`
108
+ : `expected exactly one ${prefix}* Cloud Run service, found ${matches.length} (${matches.join(", ")}); delete the stale one first`,
109
+ };
110
+ }
111
+ return { ok: true, service };
112
+ }
113
+
114
+ export async function enableAdminDomain(opts: DomainOpts): Promise<AdminDomainResult> {
115
+ const run = opts.run ?? defaultGcloud;
116
+ const scope = [`--region=${opts.region}`, `--project=${opts.projectId}`];
117
+ const found = await findService(run, scope, "admin");
118
+ if (!found.ok) return { status: "failed", error: found.error };
119
+ return mapDomain(run, logOf(opts), scope, opts.domain, `admin.${opts.domain}`, found.service);
120
+ }
121
+
122
+ /**
123
+ * #650 — map the install's staging host (its setting, default
124
+ * `staging.<domain>`; any FQDN) to the gateway, which serves the protected
125
+ * staging site. Cloud Run refuses domain-mapped requests while a service's
126
+ * ingress is internal-only, so the gateway's ingress is opened first (the
127
+ * stack declares the same since #650; installs from before keep the old
128
+ * value until this runs). Idempotent like the admin mapping.
129
+ */
130
+ export async function enableStagingDomain(
131
+ opts: DomainOpts & { stagingHost: string },
132
+ ): Promise<StagingDomainResult> {
133
+ const run = opts.run ?? defaultGcloud;
134
+ const scope = [`--region=${opts.region}`, `--project=${opts.projectId}`];
135
+ const found = await findService(run, scope, "gateway");
136
+ if (!found.ok) return { status: "failed", error: found.error };
137
+ const ingress = await run([
138
+ "run",
139
+ "services",
140
+ "describe",
141
+ found.service,
142
+ ...scope,
143
+ '--format=value(metadata.annotations["run.googleapis.com/ingress"])',
144
+ ]);
145
+ if (!ingress.ok) {
146
+ return { status: "failed", error: `read the gateway's ingress: ${ingress.stderr.trim()}` };
147
+ }
148
+ if (ingress.stdout.trim() !== "all") {
149
+ const update = await run([
150
+ "run",
151
+ "services",
152
+ "update",
153
+ found.service,
154
+ ...scope,
155
+ "--ingress=all",
156
+ "--quiet",
157
+ ]);
158
+ if (!update.ok) {
159
+ return {
160
+ status: "failed",
161
+ error: `open the gateway's ingress for the domain mapping: ${update.stderr.trim()}`,
162
+ };
163
+ }
164
+ }
165
+ // Search Console verification: the install domain covers its own
166
+ // subdomains; a host on another domain is verified itself.
167
+ const verifyDomain =
168
+ opts.stagingHost === opts.domain || opts.stagingHost.endsWith(`.${opts.domain}`)
169
+ ? opts.domain
170
+ : opts.stagingHost;
171
+ return mapDomain(run, logOf(opts), scope, verifyDomain, opts.stagingHost, found.service);
172
+ }
173
+
174
+ /** Steps 2–4 of the module doc for `hostname` → `service`. */
175
+ async function mapDomain(
176
+ run: Run,
177
+ log: (message: string) => void,
178
+ scope: string[],
179
+ domain: string,
180
+ hostname: string,
181
+ service: string,
182
+ ): Promise<AdminDomainResult> {
183
+ // Fully managed Cloud Run domain mappings exist only on the beta track: the
184
+ // GA `gcloud run domain-mappings` is Cloud Run for Anthos only. Install the
185
+ // component up front instead of letting gcloud ask for it mid-command.
186
+ const beta = await ensureGcloudComponent("beta", { run, log });
187
+ if (!beta.ok) return { status: "failed", error: beta.error };
188
+
189
+ const describeArgs = [
190
+ "beta",
191
+ "run",
192
+ "domain-mappings",
193
+ "describe",
194
+ `--domain=${hostname}`,
195
+ ...scope,
196
+ "--format=json",
197
+ ];
198
+ const existing = await run(describeArgs);
199
+ if (existing.ok) {
200
+ return {
201
+ status: "mapped",
202
+ hostname,
203
+ created: false,
204
+ records: recordsOf(existing.stdout, hostname),
205
+ };
206
+ }
207
+
208
+ const verified = await run(["domains", "list-user-verified", "--format=value(id)"]);
209
+ if (!verified.ok) {
210
+ // Expired credentials or a missing gcloud component — not "unverified".
211
+ return { status: "failed", error: `list verified domains: ${verified.stderr.trim()}` };
212
+ }
213
+ const verifiedIds = verified.stdout.split("\n").map((s) => s.trim());
214
+ if (!verifiedIds.includes(domain) && !verifiedIds.includes(hostname)) {
215
+ const verify = await run(["domains", "verify", domain]);
216
+ if (!verify.ok) {
217
+ return {
218
+ status: "failed",
219
+ error: `start domain verification for ${domain}: ${verify.stderr.trim()}`,
220
+ };
221
+ }
222
+ return { status: "needs-verification", hostname, verifyDomain: domain };
223
+ }
224
+
225
+ const create = await run([
226
+ "beta",
227
+ "run",
228
+ "domain-mappings",
229
+ "create",
230
+ `--service=${service}`,
231
+ `--domain=${hostname}`,
232
+ ...scope,
233
+ "--quiet",
234
+ ]);
235
+ if (!create.ok) {
236
+ return { status: "failed", error: `create domain mapping: ${create.stderr.trim()}` };
237
+ }
238
+ const created = await run(describeArgs);
239
+ return {
240
+ status: "mapped",
241
+ hostname,
242
+ created: true,
243
+ records: created.ok ? recordsOf(created.stdout, hostname) : [],
244
+ };
245
+ }
@@ -0,0 +1,151 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * #553 — the admin memory knob (it runs the Lighthouse quality audit):
5
+ * one default for every adapter, upgrade raises existing installs to it
6
+ * (never lowers), and the cost table prices it.
7
+ */
8
+
9
+ import { describe, expect, it } from "bun:test";
10
+ import { generateDockerCompose } from "./compose.js";
11
+ import {
12
+ ADMIN_MEMORY_DEFAULT,
13
+ azureContainerAppResources,
14
+ memoryQuantityMiB,
15
+ } from "./stack-contract.js";
16
+ import { liveContainerMemory, planAdminMemory, serviceRollArgs } from "./stack-converge.js";
17
+ import { estimateGcpCost } from "./wizards/gcp-cost.js";
18
+
19
+ describe("memoryQuantityMiB", () => {
20
+ it("parses the notations the stacks and gcloud use", () => {
21
+ expect(memoryQuantityMiB("512Mi")).toBe(512);
22
+ expect(memoryQuantityMiB("2Gi")).toBe(2048);
23
+ expect(memoryQuantityMiB("1.5Gi")).toBe(1536);
24
+ expect(memoryQuantityMiB("2G")).toBe(1907);
25
+ expect(memoryQuantityMiB("2 GB")).toBeNull();
26
+ expect(memoryQuantityMiB("lots")).toBeNull();
27
+ });
28
+
29
+ it("the default is 2Gi", () => {
30
+ expect(ADMIN_MEMORY_DEFAULT).toBe("2Gi");
31
+ });
32
+ });
33
+
34
+ describe("planAdminMemory (upgrade)", () => {
35
+ it("raises an admin below the default, including Cloud Run's implicit 512Mi", () => {
36
+ expect(planAdminMemory("1Gi")).toEqual({ ok: true, flags: ["--memory=2Gi"], from: "1Gi" });
37
+ expect(planAdminMemory(null)).toEqual({ ok: true, flags: ["--memory=2Gi"], from: null });
38
+ });
39
+
40
+ it("never lowers an operator's larger value", () => {
41
+ expect(planAdminMemory("4Gi")).toEqual({ ok: true, flags: [], from: "4Gi" });
42
+ expect(planAdminMemory("2Gi")).toEqual({ ok: true, flags: [], from: "2Gi" });
43
+ });
44
+
45
+ it("leaves a value it cannot parse alone, with a reason", () => {
46
+ const r = planAdminMemory("lots");
47
+ expect(r.ok).toBe(false);
48
+ });
49
+
50
+ it("reads the live limit from `services describe` JSON", () => {
51
+ const json = JSON.stringify({
52
+ spec: { template: { spec: { containers: [{ resources: { limits: { memory: "1Gi" } } }] } } },
53
+ });
54
+ expect(liveContainerMemory(json)).toBe("1Gi");
55
+ expect(liveContainerMemory(JSON.stringify({ spec: {} }))).toBeNull();
56
+ });
57
+
58
+ it("rides the same `services update` as the image roll", () => {
59
+ const args = serviceRollArgs({
60
+ serviceName: "caelo-production-admin",
61
+ region: "europe-west1",
62
+ projectId: "acme",
63
+ imageRef: "img@sha256:abc",
64
+ serviceAccount: "sa@acme.iam.gserviceaccount.com",
65
+ envFlags: ["--update-env-vars=A=b"],
66
+ resourceFlags: ["--memory=2Gi"],
67
+ });
68
+ expect(args).toContain("--memory=2Gi");
69
+ expect(args.indexOf("--memory=2Gi")).toBeLessThan(args.indexOf("--quiet"));
70
+ });
71
+ });
72
+
73
+ describe("self-hosted compose", () => {
74
+ const base = {
75
+ domain: "acme.com",
76
+ postgresPassword: "pw",
77
+ rolePasswords: { admin: "a", public: "b", gateway: "g" },
78
+ minioRootUser: "caelo",
79
+ minioRootPassword: "pw",
80
+ caeloSecretKek: "0".repeat(64),
81
+ diskSize: "20Gi",
82
+ images: {
83
+ mode: "release" as const,
84
+ admin: `ghcr.io/caelo-cms/admin@sha256:${"a".repeat(64)}`,
85
+ gateway: `ghcr.io/caelo-cms/gateway@sha256:${"b".repeat(64)}`,
86
+ },
87
+ };
88
+
89
+ it("reserves the default admin memory, or the configured one", () => {
90
+ expect(generateDockerCompose(base)).toContain("mem_reservation: 2048m");
91
+ expect(generateDockerCompose({ ...base, adminMemory: "3Gi" })).toContain(
92
+ "mem_reservation: 3072m",
93
+ );
94
+ });
95
+
96
+ it("refuses a value that is not a memory quantity", () => {
97
+ expect(() => generateDockerCompose({ ...base, adminMemory: "plenty" })).toThrow(
98
+ /not a memory quantity/,
99
+ );
100
+ });
101
+ });
102
+
103
+ describe("GCP cost table", () => {
104
+ const inputs = {
105
+ cloudSqlTier: "db-f1-micro",
106
+ cloudSqlHa: false,
107
+ adminMinInstances: 0,
108
+ gatewayMinInstances: 0,
109
+ wafAdaptiveProtection: false,
110
+ region: "europe-west1",
111
+ };
112
+
113
+ it("shows the admin memory and a line for the quality checks", () => {
114
+ const { lines } = estimateGcpCost(inputs);
115
+ expect(lines.map((l) => l.name)).toContain("Cloud Run admin (2Gi)");
116
+ expect(lines.some((l) => l.name.startsWith("Quality checks"))).toBe(true);
117
+ });
118
+
119
+ it("prices the extra memory of always-on admin instances", () => {
120
+ const admin = (memory: string) =>
121
+ estimateGcpCost({ ...inputs, adminMinInstances: 1, adminMemory: memory }).lines.find((l) =>
122
+ l.name.startsWith("Cloud Run admin"),
123
+ )?.monthlyUsd;
124
+ expect(admin("1Gi")).toBe(16);
125
+ expect(admin("2Gi")).toBe(23);
126
+ });
127
+
128
+ it("refuses a memory value provisioning would refuse", () => {
129
+ expect(() => estimateGcpCost({ ...inputs, adminMemory: "lots" })).toThrow("adminMemory");
130
+ });
131
+
132
+ it("shows the install region, and flags rates priced for another region (#607)", () => {
133
+ expect(estimateGcpCost(inputs).regionNote).toBe("Region: europe-west1 (fixed after install)");
134
+ const other = estimateGcpCost({ ...inputs, region: "us-east4" }).regionNote;
135
+ expect(other).toContain("Region: us-east4");
136
+ expect(other).toContain("europe-west1 list rates");
137
+ });
138
+ });
139
+
140
+ describe("Azure Container Apps sizes", () => {
141
+ it("pairs the memory with its vCPU", () => {
142
+ expect(azureContainerAppResources("2Gi")).toEqual({ cpu: 1, memory: "2.0Gi" });
143
+ expect(azureContainerAppResources("1536Mi")).toEqual({ cpu: 0.75, memory: "1.5Gi" });
144
+ });
145
+
146
+ it("refuses sizes Container Apps does not offer", () => {
147
+ for (const q of ["2G", "3.2Gi", "16Gi", "256Mi", "lots"]) {
148
+ expect(() => azureContainerAppResources(q)).toThrow("0.5Gi to 8Gi");
149
+ }
150
+ });
151
+ });