@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,61 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * The gcloud runners against a fake `gcloud` on PATH: they must never leave
5
+ * gcloud able to wait on a prompt nobody sees (regression: `admin-domain
6
+ * enable` hung on gcloud's hidden "install the beta component? (Y/n)").
7
+ */
8
+
9
+ import { afterAll, beforeAll, describe, expect, it } from "bun:test";
10
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { gcloud, gcloudEnv } from "./gcloud.js";
14
+ import { runCli } from "./regions.js";
15
+
16
+ // Echoes the prompt switch, then waits for one line on stdin, like gcloud's
17
+ // "(Y/n)" prompt does. With an open, never-ended stdin this blocks forever.
18
+ const FAKE_GCLOUD = `#!/bin/sh
19
+ echo "prompts=$CLOUDSDK_CORE_DISABLE_PROMPTS"
20
+ read -r answer
21
+ echo "answer=$answer"
22
+ `;
23
+
24
+ let dir = "";
25
+ const originalPath = process.env.PATH;
26
+
27
+ beforeAll(() => {
28
+ dir = mkdtempSync(join(tmpdir(), "caelo-fake-gcloud-"));
29
+ writeFileSync(join(dir, "gcloud"), FAKE_GCLOUD);
30
+ chmodSync(join(dir, "gcloud"), 0o755);
31
+ process.env.PATH = `${dir}:${originalPath ?? ""}`;
32
+ });
33
+
34
+ afterAll(() => {
35
+ process.env.PATH = originalPath;
36
+ rmSync(dir, { recursive: true, force: true });
37
+ });
38
+
39
+ describe("gcloud runners never block on a hidden prompt", () => {
40
+ it("gcloud() disables prompts and closes stdin", async () => {
41
+ const r = await gcloud(["beta", "run", "domain-mappings", "describe"]);
42
+ expect(r.stdout).toBe("prompts=1\nanswer=\n");
43
+ });
44
+
45
+ it("gcloud() still feeds stdin when the caller passes it", async () => {
46
+ const r = await gcloud(["secrets", "versions", "add"], { stdin: "payload\n" });
47
+ expect(r.stdout).toBe("prompts=1\nanswer=payload\n");
48
+ });
49
+
50
+ it("runCli() disables gcloud prompts and closes stdin", async () => {
51
+ const r = await runCli("gcloud", ["config", "get-value", "run/region"]);
52
+ expect(r.stdout).toBe("prompts=1\nanswer=\n");
53
+ });
54
+
55
+ it("gcloudEnv() keeps the caller's environment", () => {
56
+ expect(gcloudEnv({ HOME: "/home/x" })).toEqual({
57
+ HOME: "/home/x",
58
+ CLOUDSDK_CORE_DISABLE_PROMPTS: "1",
59
+ });
60
+ });
61
+ });
@@ -0,0 +1,99 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import { type GcloudResult, grantProvisionerRoles, PROVISIONER_ROLE_LIST } from "./gcloud.js";
5
+
6
+ const ok: GcloudResult = { ok: true, stdout: "", stderr: "", exitCode: 0 };
7
+ const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
8
+
9
+ const SA_NOT_YET_VISIBLE =
10
+ "ERROR: (gcloud.projects.add-iam-policy-binding) INVALID_ARGUMENT: Service account caelo-provisioner@p.iam.gserviceaccount.com does not exist.";
11
+ const PERMISSION_DENIED =
12
+ "ERROR: (gcloud.projects.add-iam-policy-binding) PERMISSION_DENIED: Policy update access denied.";
13
+
14
+ /** Fake gcloud: answers each call for `role` from its queue, `ok` once the queue is empty. */
15
+ function fakeGcloud(responses: Record<string, GcloudResult[]>) {
16
+ const calls: string[] = [];
17
+ const run = async (args: string[]): Promise<GcloudResult> => {
18
+ const role = args[args.indexOf("--role") + 1] as string;
19
+ calls.push(role);
20
+ return responses[role]?.shift() ?? ok;
21
+ };
22
+ return { run, calls };
23
+ }
24
+
25
+ const noSleep = async () => {};
26
+
27
+ describe("grantProvisionerRoles", () => {
28
+ it("binds only the given roles when asked (#696 install-job extras)", async () => {
29
+ const { run, calls } = fakeGcloud({});
30
+ const roles = ["roles/iam.roleAdmin", "roles/cloudscheduler.admin"];
31
+ const result = await grantProvisionerRoles("p", "sa@p", { run, sleep: noSleep, roles });
32
+ expect(result).toEqual({ granted: 2, failed: [] });
33
+ expect(calls).toEqual(roles);
34
+ });
35
+
36
+ it("retries a freshly created SA that IAM does not see yet (regression: first role failed right after SA create)", async () => {
37
+ const first = PROVISIONER_ROLE_LIST[0] as string;
38
+ const { run, calls } = fakeGcloud({
39
+ [first]: [fail(SA_NOT_YET_VISIBLE), fail(SA_NOT_YET_VISIBLE)],
40
+ });
41
+ const slept: number[] = [];
42
+
43
+ const result = await grantProvisionerRoles("p", "sa@p", {
44
+ run,
45
+ sleep: async (ms) => {
46
+ slept.push(ms);
47
+ },
48
+ retryDelaysMs: [10, 20, 40],
49
+ });
50
+
51
+ expect(result).toEqual({ granted: PROVISIONER_ROLE_LIST.length, failed: [] });
52
+ expect(calls.filter((r) => r === first)).toHaveLength(3);
53
+ expect(slept).toEqual([10, 20]);
54
+ });
55
+
56
+ it("retries concurrent policy changes", async () => {
57
+ const role = PROVISIONER_ROLE_LIST[1] as string;
58
+ const { run } = fakeGcloud({
59
+ [role]: [
60
+ fail(
61
+ "ERROR: ABORTED: There were concurrent policy changes. Please retry the whole read-modify-write with exponential backoff.",
62
+ ),
63
+ ],
64
+ });
65
+
66
+ const result = await grantProvisionerRoles("p", "sa@p", { run, sleep: noSleep });
67
+
68
+ expect(result.failed).toEqual([]);
69
+ });
70
+
71
+ it("does not retry a permission error and reports gcloud's message", async () => {
72
+ const role = PROVISIONER_ROLE_LIST[0] as string;
73
+ const { run, calls } = fakeGcloud({
74
+ [role]: [fail(PERMISSION_DENIED), fail(PERMISSION_DENIED)],
75
+ });
76
+
77
+ const result = await grantProvisionerRoles("p", "sa@p", { run, sleep: noSleep });
78
+
79
+ expect(calls.filter((r) => r === role)).toHaveLength(1);
80
+ expect(result.granted).toBe(PROVISIONER_ROLE_LIST.length - 1);
81
+ expect(result.failed).toEqual([{ role, error: PERMISSION_DENIED }]);
82
+ });
83
+
84
+ it("gives up after the last retry and reports the last error", async () => {
85
+ const role = PROVISIONER_ROLE_LIST[0] as string;
86
+ const { run, calls } = fakeGcloud({
87
+ [role]: Array.from({ length: 10 }, () => fail(SA_NOT_YET_VISIBLE)),
88
+ });
89
+
90
+ const result = await grantProvisionerRoles("p", "sa@p", {
91
+ run,
92
+ sleep: noSleep,
93
+ retryDelaysMs: [1, 1],
94
+ });
95
+
96
+ expect(calls.filter((r) => r === role)).toHaveLength(3);
97
+ expect(result.failed).toEqual([{ role, error: SA_NOT_YET_VISIBLE }]);
98
+ });
99
+ });
package/src/gcloud.ts ADDED
@@ -0,0 +1,322 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * Thin gcloud shell-out wrapper. The wizard composes high-level
5
+ * "create the project + link billing + enable APIs + create SA +
6
+ * grant roles" flows on top of these primitives. No key is ever minted
7
+ * for the provisioner SA: the operator impersonates it
8
+ * (provisioner-identity.ts, #696).
9
+ *
10
+ * Why shell out instead of using the GCP SDK directly:
11
+ * - the user's gcloud auth state IS the auth (no separate
12
+ * credentials handling — `gcloud` already knows the user)
13
+ * - project create + billing link must happen as the user, NOT as
14
+ * a service account (the SA doesn't exist yet at bootstrap time)
15
+ * - error messages from gcloud are operator-readable; the SDK's
16
+ * gRPC errors aren't
17
+ */
18
+
19
+ import { spawn } from "node:child_process";
20
+
21
+ export interface GcloudResult {
22
+ ok: boolean;
23
+ stdout: string;
24
+ stderr: string;
25
+ exitCode: number;
26
+ }
27
+
28
+ /**
29
+ * Environment for every non-interactive gcloud child process.
30
+ *
31
+ * `CLOUDSDK_CORE_DISABLE_PROMPTS=1` makes gcloud take the non-interactive
32
+ * path instead of asking a question nobody can see: our runners capture
33
+ * gcloud's output, so a "(Y/n)" prompt — e.g. "install the beta component?"
34
+ * — hung the CLI forever (`admin-domain enable`). With prompts disabled,
35
+ * gcloud fails fast with its error (or proceeds where the command allows it),
36
+ * and the caller reports the error. Commands that SHOULD talk to the
37
+ * operator (`gcloud auth login`) are never run through these runners; the
38
+ * wizard tells the operator to run them in their own terminal.
39
+ */
40
+ export function gcloudEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
41
+ return { ...base, CLOUDSDK_CORE_DISABLE_PROMPTS: "1" };
42
+ }
43
+
44
+ /**
45
+ * Run a gcloud command. Buffered stdout + stderr (max 4 MB each).
46
+ * Throws on spawn failure but NOT on non-zero exit — caller checks `ok`.
47
+ * stdin is always ended (after `opts.stdin`, if given), so nothing can wait on it.
48
+ * `opts.env` replaces this process's environment as the base (prompts stay off).
49
+ */
50
+ export async function gcloud(
51
+ args: string[],
52
+ opts: { stdin?: string; env?: NodeJS.ProcessEnv } = {},
53
+ ): Promise<GcloudResult> {
54
+ return new Promise((resolveResult, reject) => {
55
+ const child = spawn("gcloud", args, {
56
+ stdio: ["pipe", "pipe", "pipe"],
57
+ env: gcloudEnv(opts.env ?? process.env),
58
+ });
59
+ const stdout: Buffer[] = [];
60
+ const stderr: Buffer[] = [];
61
+ child.stdout.on("data", (chunk: Buffer) => stdout.push(chunk));
62
+ child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
63
+ child.on("error", (e) => reject(e));
64
+ child.on("close", (code) =>
65
+ resolveResult({
66
+ ok: code === 0,
67
+ stdout: Buffer.concat(stdout).toString("utf8"),
68
+ stderr: Buffer.concat(stderr).toString("utf8"),
69
+ exitCode: code ?? 1,
70
+ }),
71
+ );
72
+ child.stdin.end(opts.stdin);
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Active gcloud account. Returns `null` when no account is logged in
78
+ * — caller prompts `gcloud auth login`.
79
+ */
80
+ export async function activeAccount(): Promise<string | null> {
81
+ const r = await gcloud(["auth", "list", "--format=value(account)", "--filter=status:ACTIVE"]);
82
+ if (!r.ok) return null;
83
+ const value = r.stdout.trim();
84
+ return value.length > 0 ? value : null;
85
+ }
86
+
87
+ export interface BillingAccount {
88
+ id: string;
89
+ displayName: string;
90
+ open: boolean;
91
+ }
92
+
93
+ export async function listBillingAccounts(): Promise<BillingAccount[]> {
94
+ const r = await gcloud(["billing", "accounts", "list", "--format=json"]);
95
+ if (!r.ok) return [];
96
+ try {
97
+ const rows = JSON.parse(r.stdout) as Array<{
98
+ name: string;
99
+ displayName: string;
100
+ open: boolean;
101
+ }>;
102
+ return rows.map((row) => ({
103
+ id: row.name.replace(/^billingAccounts\//, ""),
104
+ displayName: row.displayName,
105
+ open: row.open,
106
+ }));
107
+ } catch {
108
+ return [];
109
+ }
110
+ }
111
+
112
+ export async function projectExists(projectId: string): Promise<boolean> {
113
+ const r = await gcloud(["projects", "describe", projectId, "--format=value(projectId)"]);
114
+ return r.ok && r.stdout.trim() === projectId;
115
+ }
116
+
117
+ export async function createProject(projectId: string, displayName: string): Promise<GcloudResult> {
118
+ return gcloud(["projects", "create", projectId, "--name", displayName]);
119
+ }
120
+
121
+ export async function linkBilling(
122
+ projectId: string,
123
+ billingAccountId: string,
124
+ ): Promise<GcloudResult> {
125
+ return gcloud(["billing", "projects", "link", projectId, "--billing-account", billingAccountId]);
126
+ }
127
+
128
+ const REQUIRED_APIS: readonly string[] = [
129
+ "compute.googleapis.com",
130
+ "sqladmin.googleapis.com",
131
+ "run.googleapis.com",
132
+ "secretmanager.googleapis.com",
133
+ "servicenetworking.googleapis.com",
134
+ "dns.googleapis.com",
135
+ "storage.googleapis.com",
136
+ "cloudresourcemanager.googleapis.com",
137
+ "iam.googleapis.com",
138
+ "cloudbuild.googleapis.com",
139
+ "artifactregistry.googleapis.com",
140
+ "bigquery.googleapis.com",
141
+ "iap.googleapis.com",
142
+ // Used by gcp.projects.ServiceIdentity to provision the IAP-managed
143
+ // service account that forwards authenticated requests to Cloud Run.
144
+ "serviceusage.googleapis.com",
145
+ // Hourly run of the operator-access sync job (operator-access.ts).
146
+ "cloudscheduler.googleapis.com",
147
+ // signJwt as caelo-mcp: how `staging-domain enable` passes the admin's
148
+ // IAP (staging-admin.ts, #678) — the same path the MCP shim takes.
149
+ "iamcredentials.googleapis.com",
150
+ // #696 — the KMS key Pulumi encrypts the stack's secrets with
151
+ // (ops-backend.ts; also enabled there for installs that predate this).
152
+ "cloudkms.googleapis.com",
153
+ ];
154
+
155
+ // v0.3.1 — gcp-firebase provider needs the Firebase Management +
156
+ // Firebase Hosting APIs on top of the gcp baseline. Without these
157
+ // the Pulumi `gcp.firebase.HostingSite` resource fails to create.
158
+ const GCP_FIREBASE_EXTRA_APIS: readonly string[] = [
159
+ "firebase.googleapis.com",
160
+ "firebasehosting.googleapis.com",
161
+ ];
162
+
163
+ export async function enableApis(
164
+ projectId: string,
165
+ provider: "gcp" | "gcp-firebase" = "gcp",
166
+ ): Promise<GcloudResult> {
167
+ const apis =
168
+ provider === "gcp-firebase" ? [...REQUIRED_APIS, ...GCP_FIREBASE_EXTRA_APIS] : REQUIRED_APIS;
169
+ return gcloud(["services", "enable", ...apis, "--project", projectId]);
170
+ }
171
+
172
+ export async function serviceAccountExists(projectId: string, saEmail: string): Promise<boolean> {
173
+ const r = await gcloud([
174
+ "iam",
175
+ "service-accounts",
176
+ "describe",
177
+ saEmail,
178
+ "--project",
179
+ projectId,
180
+ "--format=value(email)",
181
+ ]);
182
+ return r.ok && r.stdout.trim() === saEmail;
183
+ }
184
+
185
+ export async function createServiceAccount(
186
+ projectId: string,
187
+ accountId: string,
188
+ displayName: string,
189
+ ): Promise<GcloudResult> {
190
+ return gcloud([
191
+ "iam",
192
+ "service-accounts",
193
+ "create",
194
+ accountId,
195
+ "--display-name",
196
+ displayName,
197
+ "--project",
198
+ projectId,
199
+ ]);
200
+ }
201
+
202
+ const PROVISIONER_ROLES: readonly string[] = [
203
+ "roles/run.admin",
204
+ "roles/cloudsql.admin",
205
+ "roles/storage.admin",
206
+ "roles/secretmanager.admin",
207
+ "roles/iam.serviceAccountUser",
208
+ "roles/compute.networkAdmin",
209
+ "roles/dns.admin",
210
+ "roles/servicenetworking.networksAdmin",
211
+ "roles/iam.serviceAccountTokenCreator",
212
+ "roles/cloudbuild.builds.editor",
213
+ "roles/artifactregistry.admin",
214
+ "roles/compute.securityAdmin",
215
+ "roles/iam.serviceAccountAdmin",
216
+ "roles/bigquery.admin",
217
+ "roles/iap.admin",
218
+ // compute.admin includes RegionNetworkEndpointGroups + URL maps +
219
+ // BackendService variants the LB needs. compute.networkAdmin alone
220
+ // doesn't cover NEG create.
221
+ "roles/compute.admin",
222
+ // logging.configWriter creates ProjectSink (BigQuery edge logs).
223
+ "roles/logging.configWriter",
224
+ // serviceusage.serviceUsageAdmin lets us trigger the IAP managed
225
+ // service identity (gcp.projects.ServiceIdentity).
226
+ "roles/serviceusage.serviceUsageAdmin",
227
+ // v0.3.6 — Firebase Hosting + Firebase project administration
228
+ // for the gcp-firebase provider variant. The stack's
229
+ // `gcp.firebase.Project` (which adds Firebase services to a GCP
230
+ // project) + `gcp.firebase.HostingSite` need these roles.
231
+ // Granting on gcp installs is a harmless no-op since those
232
+ // resources aren't created on that stack.
233
+ "roles/firebase.admin",
234
+ "roles/firebasehosting.admin",
235
+ // v0.3.7 — project-level IAM binding admin. Required so the
236
+ // provisioner SA can grant other SAs roles AT THE PROJECT LEVEL
237
+ // via `gcp.projects.IAMMember`. The gcp-firebase stack uses
238
+ // this for `roles/firebasehosting.admin` on the run SA so it
239
+ // can deploy to Firebase Hosting. Without this role the
240
+ // provisioner gets a 403 "Policy update access denied" when
241
+ // creating project-level IAM bindings. Secret/bucket/SA-level
242
+ // bindings already work via the per-resource admin roles above.
243
+ "roles/resourcemanager.projectIamAdmin",
244
+ ];
245
+
246
+ /**
247
+ * IAM errors that clear up on their own within seconds:
248
+ * - a service account created moments ago is not yet visible to the
249
+ * IAM policy backend ("Service account ... does not exist") — this
250
+ * hits the FIRST binding right after `createServiceAccount`
251
+ * - Google's service agents write the project policy concurrently
252
+ * right after `enableApis` ("There were concurrent policy changes")
253
+ * Anything else (PERMISSION_DENIED, bad role name) fails immediately.
254
+ */
255
+ const TRANSIENT_IAM_ERROR =
256
+ /does not exist|concurrent policy changes|ABORTED|UNAVAILABLE|deadline exceeded/i;
257
+
258
+ const GRANT_RETRY_DELAYS_MS: readonly number[] = [2_000, 4_000, 8_000, 16_000];
259
+
260
+ interface RoleGrantFailure {
261
+ role: string;
262
+ /** Last gcloud stderr for this role, so the operator sees WHY. */
263
+ error: string;
264
+ }
265
+
266
+ /**
267
+ * Bind every role the GCP stack provisioner SA needs. Idempotent —
268
+ * gcloud silently no-ops a binding that already exists. Transient IAM
269
+ * errors are retried with backoff; `opts` exists for tests.
270
+ */
271
+ export async function grantProvisionerRoles(
272
+ projectId: string,
273
+ saEmail: string,
274
+ opts: {
275
+ run?: typeof gcloud;
276
+ sleep?: (ms: number) => Promise<void>;
277
+ retryDelaysMs?: readonly number[];
278
+ /** Roles to bind instead of the provisioner's base set (the install job's extras). */
279
+ roles?: readonly string[];
280
+ } = {},
281
+ ): Promise<{ granted: number; failed: RoleGrantFailure[] }> {
282
+ const run = opts.run ?? gcloud;
283
+ const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
284
+ const retryDelaysMs = opts.retryDelaysMs ?? GRANT_RETRY_DELAYS_MS;
285
+ let granted = 0;
286
+ const failed: RoleGrantFailure[] = [];
287
+ for (const role of opts.roles ?? PROVISIONER_ROLES) {
288
+ const args = [
289
+ "projects",
290
+ "add-iam-policy-binding",
291
+ projectId,
292
+ "--member",
293
+ `serviceAccount:${saEmail}`,
294
+ "--role",
295
+ role,
296
+ "--condition=None",
297
+ "--quiet",
298
+ ];
299
+ let r = await run(args);
300
+ for (const delay of retryDelaysMs) {
301
+ if (r.ok || !TRANSIENT_IAM_ERROR.test(r.stderr)) break;
302
+ await sleep(delay);
303
+ r = await run(args);
304
+ }
305
+ if (r.ok) granted++;
306
+ else failed.push({ role, error: r.stderr.trim() });
307
+ }
308
+ return { granted, failed };
309
+ }
310
+
311
+ export const REQUIRED_API_LIST = REQUIRED_APIS;
312
+ export const GCP_FIREBASE_EXTRA_API_LIST = GCP_FIREBASE_EXTRA_APIS;
313
+ export const PROVISIONER_ROLE_LIST = PROVISIONER_ROLES;
314
+
315
+ // v0.3.12 — removed createFirebaseHostingIdentity. The Service
316
+ // Usage GenerateServiceIdentity API doesn't support
317
+ // `firebasehosting.googleapis.com`, and `gcloud beta services
318
+ // identity create` would have returned the same error after a
319
+ // 2-min beta-component download. The SA gets auto-created lazily
320
+ // by Firebase Hosting on first deploy. We grant run.invoker at
321
+ // the project level instead of service level so the binding
322
+ // doesn't require the SA to exist at bind time.
@@ -0,0 +1,135 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * Deterministic names of the GCP resources the `gcp` and `gcp-firebase`
5
+ * Pulumi stacks create. The stacks build these names with the helpers below
6
+ * and `cms-provision upgrade` addresses the same resources through gcloud, so
7
+ * the two can never disagree about which bucket, secret or service account
8
+ * they mean.
9
+ *
10
+ * Only names that are fixed strings belong here. Pulumi auto-names (the
11
+ * random suffix on Cloud Run services, backend buckets, ...) are discovered at
12
+ * upgrade time instead.
13
+ *
14
+ * Pure module — the stacks import it from `dist/`, so it must not pull in
15
+ * the CLI's dependencies.
16
+ */
17
+
18
+ /** The Pulumi stack name the wizard provisions and `upgrade` operates on. */
19
+ export const GCP_STACK_ENV = "production";
20
+
21
+ /** Account id of the service account external MCP clients sign as (issue #37). */
22
+ export const MCP_IAP_SERVICE_ACCOUNT_ID = "caelo-mcp";
23
+
24
+ /** Email of the MCP service account in `projectId`. */
25
+ export function mcpIapServiceAccountEmail(projectId: string): string {
26
+ return `${MCP_IAP_SERVICE_ACCOUNT_ID}@${projectId}.iam.gserviceaccount.com`;
27
+ }
28
+
29
+ /** `caelo-<env>`: prefix of every stack resource name. */
30
+ export function gcpNamePrefix(env: string): string {
31
+ return `caelo-${env}`;
32
+ }
33
+
34
+ /** Account id of the runtime service account admin + gateway run as. */
35
+ export function runServiceAccountId(env: string): string {
36
+ return `${gcpNamePrefix(env)}-run-sa`;
37
+ }
38
+
39
+ /** Email of the runtime service account admin + gateway run as. */
40
+ export function runServiceAccountEmail(projectId: string, env: string): string {
41
+ return `${runServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
42
+ }
43
+
44
+ /**
45
+ * Account id of the service account the gateway runs as. Separate from the
46
+ * admin's so the public-facing gateway can read only the secrets it needs
47
+ * (stack-contract.ts `SERVICE_SECRET_ENV`), never the KEK or the admin-only
48
+ * secrets.
49
+ */
50
+ export function gatewayServiceAccountId(env: string): string {
51
+ return `${gcpNamePrefix(env)}-gateway-sa`;
52
+ }
53
+
54
+ /** Email of the service account the gateway runs as. */
55
+ export function gatewayServiceAccountEmail(projectId: string, env: string): string {
56
+ return `${gatewayServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
57
+ }
58
+
59
+ /**
60
+ * Account id of the `gcp` stack's static-publisher service account. Account
61
+ * ids max out at 30 chars, so the env is shortened.
62
+ */
63
+ export function staticPublisherServiceAccountId(env: string): string {
64
+ const short = env === "production" ? "prod" : env === "staging" ? "stg" : "dev";
65
+ return `caelo-${short}-publisher`;
66
+ }
67
+
68
+ /** Globally unique GCS bucket name the stacks give each bucket. */
69
+ export function gcpBucketName(
70
+ projectId: string,
71
+ env: string,
72
+ bucket: "media" | "static" | "staging",
73
+ ): string {
74
+ return `${projectId}-${gcpNamePrefix(env)}-${bucket}`;
75
+ }
76
+
77
+ /** Secret Manager secret id for one of the stack's runtime secrets. */
78
+ export function gcpSecretId(env: string, name: string): string {
79
+ return `${gcpNamePrefix(env)}-${name}`;
80
+ }
81
+
82
+ /** Cloud Run job that keeps Google IAP operator access in sync (operator-access.ts). */
83
+ export function operatorAccessJobName(env: string): string {
84
+ return `${gcpNamePrefix(env)}-operator-access-sync`;
85
+ }
86
+
87
+ /** Full resource name of the operator-access sync job, as the admin's env carries it. */
88
+ export function operatorAccessJobResource(projectId: string, region: string, env: string): string {
89
+ return `projects/${projectId}/locations/${region}/jobs/${operatorAccessJobName(env)}`;
90
+ }
91
+
92
+ /**
93
+ * Account id of the sync job's own service account — the only principal that
94
+ * may change the admin's IAP binding and the caelo-mcp token-creator binding.
95
+ */
96
+ export function operatorAccessServiceAccountId(env: string): string {
97
+ return `${gcpNamePrefix(env)}-opaccess`;
98
+ }
99
+
100
+ /** Email of the sync job's service account. */
101
+ export function operatorAccessServiceAccountEmail(projectId: string, env: string): string {
102
+ return `${operatorAccessServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
103
+ }
104
+
105
+ /** Cloud Scheduler job that runs the sync job hourly (self-healing). */
106
+ export function operatorAccessScheduleName(env: string): string {
107
+ return `${gcpNamePrefix(env)}-operator-access-hourly`;
108
+ }
109
+
110
+ /**
111
+ * Cloud Run job that installs Caelo releases from the admin (epic #696):
112
+ * the admin may only start it, without overrides (upgrader-setup.ts).
113
+ */
114
+ export function upgraderJobName(env: string): string {
115
+ return `${gcpNamePrefix(env)}-upgrader`;
116
+ }
117
+
118
+ /** Full resource name of the upgrader job, as the admin's env carries it. */
119
+ export function upgraderJobResource(projectId: string, region: string, env: string): string {
120
+ return `projects/${projectId}/locations/${region}/jobs/${upgraderJobName(env)}`;
121
+ }
122
+
123
+ /**
124
+ * Account id of the upgrader job's own service account: may roll the two
125
+ * Cloud Run services, run the migration job and back up Cloud SQL, never
126
+ * change IAM, service accounts or secrets (upgrader-setup.ts).
127
+ */
128
+ export function upgraderServiceAccountId(env: string): string {
129
+ return `${gcpNamePrefix(env)}-upgrader`;
130
+ }
131
+
132
+ /** Email of the upgrader job's service account. */
133
+ export function upgraderServiceAccountEmail(projectId: string, env: string): string {
134
+ return `${upgraderServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
135
+ }