@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,243 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * Where install.json and progress.json are read from and written to (#696).
5
+ *
6
+ * - {@link LocalStateStore}: `~/.caelo-<id>/` — every install before #696,
7
+ * and self-hosted always.
8
+ * - {@link GcsStateStore}: the install's ops bucket
9
+ * (`gs://<project>-caelo-ops/install/`), which is authoritative. A local
10
+ * copy is kept in `~/.caelo-<id>/` so `listInstalls` and the install
11
+ * picker work offline, but every read in a process starts from the
12
+ * bucket and every write goes to the bucket first.
13
+ *
14
+ * Both stores refuse to write secret material (checkpoint-secrets.ts, #656 /
15
+ * #659): install.json and progress.json are read by tooling, backups and
16
+ * agents, and a bucket copy is no place for a database password either. The
17
+ * guard sits inside each store's `write`, so no caller can skip it.
18
+ *
19
+ * The interface is synchronous on purpose: install-state.ts's callers (the
20
+ * wizard steps, `markStepDone`, the lifecycle commands) are synchronous, and
21
+ * the bucket store shells out to `gcloud storage` synchronously.
22
+ */
23
+
24
+ import { spawnSync } from "node:child_process";
25
+ import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
26
+ import { hostname } from "node:os";
27
+ import { join } from "node:path";
28
+ import { assertNoCheckpointSecrets, assertNoSecrets } from "./checkpoint-secrets.js";
29
+ import { type GcloudResult, gcloudEnv } from "./gcloud.js";
30
+ import { OPS_INSTALL_PREFIX } from "./state-backend.js";
31
+
32
+ /** The two documents a store holds. */
33
+ export type StateFile = "install.json" | "progress.json";
34
+
35
+ /** Reads and writes an install's metadata documents. */
36
+ export interface StateStore {
37
+ readonly kind: "local" | "gcs";
38
+ /** The document's text, or null when it does not exist. */
39
+ read(file: StateFile): string | null;
40
+ /** Replace the document. Refuses secret material. */
41
+ write(file: StateFile, value: unknown): void;
42
+ }
43
+
44
+ /** A synchronous gcloud invocation; `input` is fed on stdin. Tests pass a fake. */
45
+ export type SyncGcloud = (args: string[], input?: string) => GcloudResult;
46
+
47
+ /** The real {@link SyncGcloud}: `gcloud` with prompts disabled. */
48
+ export const realSyncGcloud: SyncGcloud = (args, input) => {
49
+ const r = spawnSync("gcloud", args, {
50
+ env: gcloudEnv(),
51
+ encoding: "utf8",
52
+ maxBuffer: 16 * 1024 * 1024,
53
+ ...(input !== undefined ? { input } : { stdio: ["ignore", "pipe", "pipe"] }),
54
+ });
55
+ if (r.error) throw r.error;
56
+ return {
57
+ ok: r.status === 0,
58
+ stdout: r.stdout ?? "",
59
+ stderr: r.stderr ?? "",
60
+ exitCode: r.status ?? 1,
61
+ };
62
+ };
63
+
64
+ /**
65
+ * Serialize a document after the checkpoint-secrets guard. install.json is
66
+ * checked for any secret; progress.json with the checkpoint rules (step
67
+ * payloads, outputs).
68
+ */
69
+ export function serializeGuarded(file: StateFile, value: unknown): string {
70
+ if (file === "install.json") assertNoSecrets(value, file);
71
+ else assertNoCheckpointSecrets(value, file);
72
+ return `${JSON.stringify(value, null, 2)}\n`;
73
+ }
74
+
75
+ /** Atomic mode-0600 write (temp file + rename in the same directory). */
76
+ function writeTextAtomic(path: string, text: string): void {
77
+ const tmp = `${path}.tmp-${process.pid}`;
78
+ try {
79
+ writeFileSync(tmp, text, { mode: 0o600 });
80
+ renameSync(tmp, path);
81
+ } catch (e) {
82
+ rmSync(tmp, { force: true });
83
+ throw e;
84
+ }
85
+ }
86
+
87
+ /** install.json + progress.json in `~/.caelo-<id>/`. */
88
+ export class LocalStateStore implements StateStore {
89
+ readonly kind = "local" as const;
90
+ constructor(private readonly root: string) {}
91
+
92
+ read(file: StateFile): string | null {
93
+ const path = join(this.root, file);
94
+ return existsSync(path) ? readFileSync(path, "utf8") : null;
95
+ }
96
+
97
+ write(file: StateFile, value: unknown): void {
98
+ writeTextAtomic(join(this.root, file), serializeGuarded(file, value));
99
+ }
100
+ }
101
+
102
+ /** gcloud's wording for an object that is not there. */
103
+ const NOT_FOUND = /matched no objects|No URLs matched|NotFound|404|does not exist/i;
104
+
105
+ /**
106
+ * install.json + progress.json in the ops bucket, mirrored to
107
+ * `~/.caelo-<id>/`. The first read of a document in a process fetches it
108
+ * from the bucket; later reads use the in-memory copy, which every write
109
+ * updates. A bucket error throws — the bucket is the source of truth, so
110
+ * silently using the local mirror could resume from a stale checkpoint.
111
+ */
112
+ export class GcsStateStore implements StateStore {
113
+ readonly kind = "gcs" as const;
114
+ private readonly cache = new Map<StateFile, string | null>();
115
+
116
+ constructor(
117
+ private readonly bucket: string,
118
+ private readonly root: string,
119
+ private readonly run: SyncGcloud = realSyncGcloud,
120
+ ) {}
121
+
122
+ /** `gs://<bucket>/install/<file>`. */
123
+ objectUrl(file: StateFile): string {
124
+ return `gs://${this.bucket}/${OPS_INSTALL_PREFIX}/${file}`;
125
+ }
126
+
127
+ /**
128
+ * The document as the bucket holds it right now, without caching it or
129
+ * touching the local mirror — for looking at a bucket this machine may
130
+ * not own (the wizard's "is this project already an install?" check).
131
+ */
132
+ peek(file: StateFile): string | null {
133
+ const r = this.run(["storage", "cat", this.objectUrl(file)]);
134
+ if (r.ok) return r.stdout;
135
+ if (NOT_FOUND.test(r.stderr)) return null;
136
+ throw new Error(
137
+ `Could not read ${this.objectUrl(file)}: ${r.stderr.trim() || `gcloud exited ${r.exitCode}`}. Check that you are signed in (gcloud auth login) as an account with access to the install's ops bucket.`,
138
+ );
139
+ }
140
+
141
+ read(file: StateFile): string | null {
142
+ if (this.cache.has(file)) return this.cache.get(file) ?? null;
143
+ const text = this.peek(file);
144
+ this.cache.set(file, text);
145
+ if (text !== null) writeTextAtomic(join(this.root, file), text);
146
+ return text;
147
+ }
148
+
149
+ write(file: StateFile, value: unknown): void {
150
+ const text = serializeGuarded(file, value);
151
+ const r = this.run(
152
+ ["storage", "cp", "-", this.objectUrl(file), "--content-type=application/json"],
153
+ text,
154
+ );
155
+ if (!r.ok) {
156
+ throw new Error(
157
+ `Could not write ${this.objectUrl(file)}: ${r.stderr.trim() || `gcloud exited ${r.exitCode}`}. Nothing was changed locally; fix access to the ops bucket and re-run.`,
158
+ );
159
+ }
160
+ this.cache.set(file, text);
161
+ writeTextAtomic(join(this.root, file), text);
162
+ }
163
+ }
164
+
165
+ /** `gs://<bucket>/install/state.lock` — see {@link acquireBucketStateLock}. */
166
+ export function bucketStateLockUrl(bucket: string): string {
167
+ return `gs://${bucket}/${OPS_INSTALL_PREFIX}/state.lock`;
168
+ }
169
+
170
+ /** Who holds a lock, for the refusal message. */
171
+ function lockBody(purpose: string): string {
172
+ return JSON.stringify({
173
+ purpose,
174
+ host: hostname(),
175
+ pid: process.pid,
176
+ startedAt: new Date().toISOString(),
177
+ });
178
+ }
179
+
180
+ /**
181
+ * The install's state lock in its ops bucket, created only if absent
182
+ * (`--if-generation-match=0`). Held by every writer of a bucket-backed
183
+ * install's state — an installer run's `pulumi up`, `state migrate` — so
184
+ * none writes a backend another is switching away from. A lock left by a
185
+ * crashed run is reported with the command to remove it, never broken
186
+ * automatically.
187
+ *
188
+ * @returns the release function
189
+ */
190
+ export function acquireBucketStateLock(
191
+ bucket: string,
192
+ purpose: string,
193
+ run: SyncGcloud = realSyncGcloud,
194
+ ): () => void {
195
+ const url = bucketStateLockUrl(bucket);
196
+ const r = run(["storage", "cp", "-", url, "--if-generation-match=0"], lockBody(purpose));
197
+ if (!r.ok) {
198
+ if (/PreconditionFailed|412|At least one of the pre-conditions/i.test(r.stderr)) {
199
+ const held = run(["storage", "cat", url]);
200
+ throw new Error(
201
+ `Another run is changing this install's state (${held.ok ? held.stdout.trim() : url}). Re-run once it has finished; if none is running, remove the stale lock: gcloud storage rm ${url}`,
202
+ );
203
+ }
204
+ throw new Error(`Could not take the state lock ${url}: ${r.stderr.trim()}`);
205
+ }
206
+ return () => {
207
+ const rm = run(["storage", "rm", url]);
208
+ if (!rm.ok) throw new Error(`Could not release ${url}; remove it: gcloud storage rm ${url}`);
209
+ };
210
+ }
211
+
212
+ /**
213
+ * The same lock for an install whose state is on this machine:
214
+ * `~/.caelo-<id>/state.lock`, created exclusively.
215
+ *
216
+ * @returns the release function
217
+ */
218
+ export function acquireLocalStateLock(root: string, purpose: string): () => void {
219
+ const path = join(root, "state.lock");
220
+ try {
221
+ writeFileSync(path, lockBody(purpose), { flag: "wx", mode: 0o600 });
222
+ } catch (e) {
223
+ if ((e as NodeJS.ErrnoException).code === "EEXIST") {
224
+ throw new Error(
225
+ `Another run is changing this install's state (${readFileSync(path, "utf8").trim()}). Re-run once it has finished; if none is running, remove the stale lock: rm ${path}`,
226
+ );
227
+ }
228
+ throw e;
229
+ }
230
+ return () => rmSync(path, { force: true });
231
+ }
232
+
233
+ /** The state lock for whichever backend `bucket` (gcs) or `root` (local) names. */
234
+ export function acquireStateLock(
235
+ backend: { readonly kind: "local" } | { readonly kind: "gcs"; readonly bucket: string },
236
+ root: string,
237
+ purpose: string,
238
+ run: SyncGcloud = realSyncGcloud,
239
+ ): () => void {
240
+ return backend.kind === "gcs"
241
+ ? acquireBucketStateLock(backend.bucket, purpose, run)
242
+ : acquireLocalStateLock(root, purpose);
243
+ }