@caelo-cms/provisioning 0.10.47 → 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 (315) hide show
  1. package/dist/agent/apply.d.ts +45 -0
  2. package/dist/agent/apply.d.ts.map +1 -0
  3. package/dist/agent/apply.js +83 -0
  4. package/dist/agent/apply.js.map +1 -0
  5. package/dist/agent/cli-provenance.d.ts +42 -0
  6. package/dist/agent/cli-provenance.d.ts.map +1 -0
  7. package/dist/agent/cli-provenance.js +138 -0
  8. package/dist/agent/cli-provenance.js.map +1 -0
  9. package/dist/agent/command.d.ts +79 -0
  10. package/dist/agent/command.d.ts.map +1 -0
  11. package/dist/agent/command.js +454 -0
  12. package/dist/agent/command.js.map +1 -0
  13. package/dist/agent/config.d.ts +30 -0
  14. package/dist/agent/config.d.ts.map +1 -0
  15. package/dist/agent/config.js +60 -0
  16. package/dist/agent/config.js.map +1 -0
  17. package/dist/agent/lock.d.ts +13 -0
  18. package/dist/agent/lock.d.ts.map +1 -0
  19. package/dist/agent/lock.js +100 -0
  20. package/dist/agent/lock.js.map +1 -0
  21. package/dist/agent/queue.d.ts +35 -0
  22. package/dist/agent/queue.d.ts.map +1 -0
  23. package/dist/agent/queue.js +50 -0
  24. package/dist/agent/queue.js.map +1 -0
  25. package/dist/agent/run.d.ts +97 -0
  26. package/dist/agent/run.d.ts.map +1 -0
  27. package/dist/agent/run.js +198 -0
  28. package/dist/agent/run.js.map +1 -0
  29. package/dist/agent/systemd.d.ts +55 -0
  30. package/dist/agent/systemd.d.ts.map +1 -0
  31. package/dist/agent/systemd.js +171 -0
  32. package/dist/agent/systemd.js.map +1 -0
  33. package/dist/agent/units.d.ts +44 -0
  34. package/dist/agent/units.d.ts.map +1 -0
  35. package/dist/agent/units.js +127 -0
  36. package/dist/agent/units.js.map +1 -0
  37. package/dist/agent-db.js +18362 -0
  38. package/dist/cli.js +119 -16
  39. package/dist/cli.js.map +1 -1
  40. package/dist/cloudsql-iam-user.d.ts +39 -0
  41. package/dist/cloudsql-iam-user.d.ts.map +1 -0
  42. package/dist/cloudsql-iam-user.js +119 -0
  43. package/dist/cloudsql-iam-user.js.map +1 -0
  44. package/dist/compose.d.ts +16 -0
  45. package/dist/compose.d.ts.map +1 -1
  46. package/dist/compose.js +26 -1
  47. package/dist/compose.js.map +1 -1
  48. package/dist/dns/index.d.ts +2 -0
  49. package/dist/dns/index.d.ts.map +1 -1
  50. package/dist/dns/index.js +3 -2
  51. package/dist/dns/index.js.map +1 -1
  52. package/dist/dns/manual.d.ts +5 -0
  53. package/dist/dns/manual.d.ts.map +1 -1
  54. package/dist/dns/manual.js +28 -12
  55. package/dist/dns/manual.js.map +1 -1
  56. package/dist/gcloud.d.ts +2 -0
  57. package/dist/gcloud.d.ts.map +1 -1
  58. package/dist/gcloud.js +1 -1
  59. package/dist/gcloud.js.map +1 -1
  60. package/dist/gcp-names.d.ts +15 -0
  61. package/dist/gcp-names.d.ts.map +1 -1
  62. package/dist/gcp-names.js +23 -0
  63. package/dist/gcp-names.js.map +1 -1
  64. package/dist/index.d.ts +6 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +4 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/infra-contract.d.ts +1 -1
  69. package/dist/infra-contract.js +7 -1
  70. package/dist/infra-contract.js.map +1 -1
  71. package/dist/install-job-runner.d.ts +13 -0
  72. package/dist/install-job-runner.d.ts.map +1 -0
  73. package/dist/install-job-runner.js +155 -0
  74. package/dist/install-job-runner.js.map +1 -0
  75. package/dist/install-job.d.ts +210 -0
  76. package/dist/install-job.d.ts.map +1 -0
  77. package/dist/install-job.js +449 -0
  78. package/dist/install-job.js.map +1 -0
  79. package/dist/install-state.d.ts +10 -1
  80. package/dist/install-state.d.ts.map +1 -1
  81. package/dist/install-state.js +10 -2
  82. package/dist/install-state.js.map +1 -1
  83. package/dist/lifecycle.d.ts.map +1 -1
  84. package/dist/lifecycle.js +19 -4
  85. package/dist/lifecycle.js.map +1 -1
  86. package/dist/migration-runner.d.ts +5 -0
  87. package/dist/migration-runner.d.ts.map +1 -1
  88. package/dist/migration-runner.js +1 -1
  89. package/dist/migration-runner.js.map +1 -1
  90. package/dist/operator-access.d.ts +14 -8
  91. package/dist/operator-access.d.ts.map +1 -1
  92. package/dist/operator-access.js +16 -102
  93. package/dist/operator-access.js.map +1 -1
  94. package/dist/run-mode.d.ts +27 -0
  95. package/dist/run-mode.d.ts.map +1 -0
  96. package/dist/run-mode.js +22 -0
  97. package/dist/run-mode.js.map +1 -0
  98. package/dist/runtime-secrets.d.ts +19 -0
  99. package/dist/runtime-secrets.d.ts.map +1 -1
  100. package/dist/runtime-secrets.js +44 -0
  101. package/dist/runtime-secrets.js.map +1 -1
  102. package/dist/self-hosted-roles.d.ts +22 -1
  103. package/dist/self-hosted-roles.d.ts.map +1 -1
  104. package/dist/self-hosted-roles.js +81 -12
  105. package/dist/self-hosted-roles.js.map +1 -1
  106. package/dist/self-hosted-upgrade.d.ts +17 -0
  107. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  108. package/dist/self-hosted-upgrade.js +16 -1
  109. package/dist/self-hosted-upgrade.js.map +1 -1
  110. package/dist/stack-contract.d.ts +6 -0
  111. package/dist/stack-contract.d.ts.map +1 -1
  112. package/dist/stack-contract.js +10 -1
  113. package/dist/stack-contract.js.map +1 -1
  114. package/dist/upgrade/converge.d.ts +6 -0
  115. package/dist/upgrade/converge.d.ts.map +1 -1
  116. package/dist/upgrade/converge.js +59 -2
  117. package/dist/upgrade/converge.js.map +1 -1
  118. package/dist/upgrade/gcp-lookup.d.ts +6 -0
  119. package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
  120. package/dist/upgrade/gcp-lookup.js +25 -0
  121. package/dist/upgrade/gcp-lookup.js.map +1 -1
  122. package/dist/upgrade/release-images.d.ts +12 -0
  123. package/dist/upgrade/release-images.d.ts.map +1 -1
  124. package/dist/upgrade/release-images.js +31 -0
  125. package/dist/upgrade/release-images.js.map +1 -1
  126. package/dist/upgrade/upgrade.d.ts +5 -0
  127. package/dist/upgrade/upgrade.d.ts.map +1 -1
  128. package/dist/upgrade/upgrade.js +28 -1
  129. package/dist/upgrade/upgrade.js.map +1 -1
  130. package/dist/upgrader-assess.d.ts +82 -0
  131. package/dist/upgrader-assess.d.ts.map +1 -0
  132. package/dist/upgrader-assess.js +116 -0
  133. package/dist/upgrader-assess.js.map +1 -0
  134. package/dist/upgrader-gcp.d.ts +17 -0
  135. package/dist/upgrader-gcp.d.ts.map +1 -0
  136. package/dist/upgrader-gcp.js +87 -0
  137. package/dist/upgrader-gcp.js.map +1 -0
  138. package/dist/upgrader-job.d.ts +131 -0
  139. package/dist/upgrader-job.d.ts.map +1 -0
  140. package/dist/upgrader-job.js +259 -0
  141. package/dist/upgrader-job.js.map +1 -0
  142. package/dist/upgrader-reporter.d.ts +35 -0
  143. package/dist/upgrader-reporter.d.ts.map +1 -0
  144. package/dist/upgrader-reporter.js +79 -0
  145. package/dist/upgrader-reporter.js.map +1 -0
  146. package/dist/upgrader-setup.d.ts +59 -0
  147. package/dist/upgrader-setup.d.ts.map +1 -0
  148. package/dist/upgrader-setup.js +308 -0
  149. package/dist/upgrader-setup.js.map +1 -0
  150. package/dist/wizard.d.ts +6 -0
  151. package/dist/wizard.d.ts.map +1 -1
  152. package/dist/wizard.js +32 -4
  153. package/dist/wizard.js.map +1 -1
  154. package/dist/wizards/gcp-firebase.d.ts +3 -0
  155. package/dist/wizards/gcp-firebase.d.ts.map +1 -1
  156. package/dist/wizards/gcp-firebase.js +1 -0
  157. package/dist/wizards/gcp-firebase.js.map +1 -1
  158. package/dist/wizards/gcp-install-job.d.ts +74 -0
  159. package/dist/wizards/gcp-install-job.d.ts.map +1 -0
  160. package/dist/wizards/gcp-install-job.js +398 -0
  161. package/dist/wizards/gcp-install-job.js.map +1 -0
  162. package/dist/wizards/gcp-prompts.d.ts +3 -0
  163. package/dist/wizards/gcp-prompts.d.ts.map +1 -0
  164. package/dist/wizards/gcp-prompts.js +25 -0
  165. package/dist/wizards/gcp-prompts.js.map +1 -0
  166. package/dist/wizards/gcp-pulumi.d.ts +4 -2
  167. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  168. package/dist/wizards/gcp-pulumi.js +1 -1
  169. package/dist/wizards/gcp-pulumi.js.map +1 -1
  170. package/dist/wizards/gcp.d.ts +40 -0
  171. package/dist/wizards/gcp.d.ts.map +1 -1
  172. package/dist/wizards/gcp.js +223 -59
  173. package/dist/wizards/gcp.js.map +1 -1
  174. package/package.json +6 -4
  175. package/src/adapter.ts +97 -0
  176. package/src/adc-impersonation.ts +176 -0
  177. package/src/admin-domain.test.ts +254 -0
  178. package/src/admin-domain.ts +245 -0
  179. package/src/admin-memory.test.ts +151 -0
  180. package/src/agent/apply.test.ts +185 -0
  181. package/src/agent/apply.ts +124 -0
  182. package/src/agent/cli-provenance.test.ts +159 -0
  183. package/src/agent/cli-provenance.ts +191 -0
  184. package/src/agent/command.test.ts +77 -0
  185. package/src/agent/command.ts +564 -0
  186. package/src/agent/config.ts +86 -0
  187. package/src/agent/lock-config.test.ts +97 -0
  188. package/src/agent/lock.ts +110 -0
  189. package/src/agent/queue.test.ts +10 -0
  190. package/src/agent/queue.ts +91 -0
  191. package/src/agent/run.test.ts +382 -0
  192. package/src/agent/run.ts +295 -0
  193. package/src/agent/systemd.ts +209 -0
  194. package/src/agent/units.test.ts +223 -0
  195. package/src/agent/units.ts +163 -0
  196. package/src/azure-front-door.test.ts +119 -0
  197. package/src/azure-front-door.ts +56 -0
  198. package/src/bootstrap-token.ts +20 -0
  199. package/src/caddy.ts +112 -0
  200. package/src/cdn-copy.ts +91 -0
  201. package/src/cert-wait.test.ts +194 -0
  202. package/src/cert-wait.ts +163 -0
  203. package/src/checkpoint-secrets.test.ts +380 -0
  204. package/src/checkpoint-secrets.ts +283 -0
  205. package/src/cli-routing.test.ts +45 -0
  206. package/src/cli-routing.ts +43 -0
  207. package/src/cli.ts +1491 -0
  208. package/src/cloudsql-iam-user.ts +147 -0
  209. package/src/compose.ts +334 -0
  210. package/src/default-firewall.test.ts +411 -0
  211. package/src/default-firewall.ts +342 -0
  212. package/src/deployed-release.test.ts +231 -0
  213. package/src/deployed-release.ts +220 -0
  214. package/src/dns/cloudflare.ts +182 -0
  215. package/src/dns/groups.test.ts +76 -0
  216. package/src/dns/groups.ts +86 -0
  217. package/src/dns/index.ts +56 -0
  218. package/src/dns/manual.ts +130 -0
  219. package/src/dns/types.ts +27 -0
  220. package/src/gateway-credentials.test.ts +180 -0
  221. package/src/gateway-credentials.ts +144 -0
  222. package/src/gcloud-components.test.ts +76 -0
  223. package/src/gcloud-components.ts +85 -0
  224. package/src/gcloud-retry.ts +41 -0
  225. package/src/gcloud-runner.test.ts +61 -0
  226. package/src/gcloud.test.ts +99 -0
  227. package/src/gcloud.ts +322 -0
  228. package/src/gcp-names.ts +135 -0
  229. package/src/index.test.ts +332 -0
  230. package/src/index.ts +114 -0
  231. package/src/infra-contract.ts +32 -0
  232. package/src/install-job-runner.ts +166 -0
  233. package/src/install-job.test.ts +566 -0
  234. package/src/install-job.ts +542 -0
  235. package/src/install-state.test.ts +239 -0
  236. package/src/install-state.ts +583 -0
  237. package/src/internal-token.ts +35 -0
  238. package/src/lifecycle.ts +900 -0
  239. package/src/mcp-iap-stacks.test.ts +58 -0
  240. package/src/mcp-iap.test.ts +149 -0
  241. package/src/mcp-iap.ts +131 -0
  242. package/src/media-volume.test.ts +296 -0
  243. package/src/migration-runner.ts +365 -0
  244. package/src/operator-access-stacks.test.ts +55 -0
  245. package/src/operator-access.test.ts +332 -0
  246. package/src/operator-access.ts +554 -0
  247. package/src/ops-backend.test.ts +320 -0
  248. package/src/ops-backend.ts +414 -0
  249. package/src/provisioner-identity.test.ts +482 -0
  250. package/src/provisioner-identity.ts +407 -0
  251. package/src/redirects-emit.ts +175 -0
  252. package/src/region-data.ts +428 -0
  253. package/src/regions.test.ts +277 -0
  254. package/src/regions.ts +370 -0
  255. package/src/release-contract.ts +57 -0
  256. package/src/release-manifest.test.ts +293 -0
  257. package/src/release-manifest.ts +430 -0
  258. package/src/run-mode.ts +40 -0
  259. package/src/runtime-secrets.test.ts +627 -0
  260. package/src/runtime-secrets.ts +754 -0
  261. package/src/self-command.test.ts +14 -0
  262. package/src/self-command.ts +21 -0
  263. package/src/self-hosted-compose.test.ts +177 -0
  264. package/src/self-hosted-release.ts +240 -0
  265. package/src/self-hosted-roles.integration.test.ts +161 -0
  266. package/src/self-hosted-roles.test.ts +124 -0
  267. package/src/self-hosted-roles.ts +203 -0
  268. package/src/self-hosted-upgrade.test.ts +394 -0
  269. package/src/self-hosted-upgrade.ts +548 -0
  270. package/src/stack-contract-parity.test.ts +199 -0
  271. package/src/stack-contract.test.ts +391 -0
  272. package/src/stack-contract.ts +888 -0
  273. package/src/stack-converge-secrets.test.ts +421 -0
  274. package/src/stack-converge.test.ts +833 -0
  275. package/src/stack-converge.ts +909 -0
  276. package/src/staging-admin.test.ts +246 -0
  277. package/src/staging-admin.ts +324 -0
  278. package/src/state-attach.test.ts +211 -0
  279. package/src/state-attach.ts +132 -0
  280. package/src/state-backend.test.ts +145 -0
  281. package/src/state-backend.ts +262 -0
  282. package/src/state-command.ts +293 -0
  283. package/src/state-migrate.test.ts +479 -0
  284. package/src/state-migrate.ts +434 -0
  285. package/src/state-pulumi.ts +289 -0
  286. package/src/state-store.test.ts +212 -0
  287. package/src/state-store.ts +243 -0
  288. package/src/upgrade/app-roll.ts +486 -0
  289. package/src/upgrade/backup.ts +109 -0
  290. package/src/upgrade/converge.ts +437 -0
  291. package/src/upgrade/gcp-lookup.ts +167 -0
  292. package/src/upgrade/health.test.ts +279 -0
  293. package/src/upgrade/health.ts +305 -0
  294. package/src/upgrade/release-images.test.ts +119 -0
  295. package/src/upgrade/release-images.ts +391 -0
  296. package/src/upgrade/reporter.ts +46 -0
  297. package/src/upgrade/upgrade.test.ts +626 -0
  298. package/src/upgrade/upgrade.ts +249 -0
  299. package/src/upgrader-assess.test.ts +265 -0
  300. package/src/upgrader-assess.ts +233 -0
  301. package/src/upgrader-gcp.ts +123 -0
  302. package/src/upgrader-job.test.ts +408 -0
  303. package/src/upgrader-job.ts +499 -0
  304. package/src/upgrader-reporter.test.ts +46 -0
  305. package/src/upgrader-reporter.ts +105 -0
  306. package/src/upgrader-setup.test.ts +169 -0
  307. package/src/upgrader-setup.ts +355 -0
  308. package/src/wizard.ts +425 -0
  309. package/src/wizards/gcp-cost.ts +188 -0
  310. package/src/wizards/gcp-firebase.ts +72 -0
  311. package/src/wizards/gcp-install-job.ts +506 -0
  312. package/src/wizards/gcp-prompts.ts +26 -0
  313. package/src/wizards/gcp-pulumi.test.ts +246 -0
  314. package/src/wizards/gcp-pulumi.ts +516 -0
  315. package/src/wizards/gcp.ts +1935 -0
@@ -0,0 +1,516 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * Pulumi Automation SDK wrapper for the GCP wizard. Replaces the
5
+ * shell-out `pulumi up` flow with a programmatic equivalent that
6
+ * streams progress events into the wizard's UI.
7
+ *
8
+ * Per CLAUDE.md §11.C: end-users never see `pulumi` directly. The
9
+ * wizard handles the state backend (state-backend.ts: the install's ops
10
+ * bucket + KMS key, or the local dir + passphrase of an older install),
11
+ * config and up; failures surface inline with provider errors.
12
+ */
13
+
14
+ import { resolve as resolvePath } from "node:path";
15
+ import * as pulumi from "@pulumi/pulumi/automation";
16
+ import { checkpointableOutputs } from "../checkpoint-secrets.js";
17
+ import { GCP_STACK_ENV } from "../gcp-names.js";
18
+ import type { ImageDigests } from "../install-state.js";
19
+ import {
20
+ AMBIENT_GOOGLE_CREDENTIAL_ENV,
21
+ withoutAmbientGoogleCredentials,
22
+ } from "../provisioner-identity.js";
23
+ import { ensureGeneratedSecrets, stackSecretReplication } from "../runtime-secrets.js";
24
+ import { ADMIN_MEMORY_DEFAULT } from "../stack-contract.js";
25
+ import type { ResolvedBackend } from "../state-backend.js";
26
+
27
+ /**
28
+ * The §11.B knobs every wizard run sets (and the cost table prices). One
29
+ * place, so `state migrate`'s verification previews set the same config the
30
+ * wizard does.
31
+ */
32
+ export const GCP_STACK_KNOB_DEFAULTS = {
33
+ cloudSqlTier: "db-f1-micro",
34
+ cloudSqlHa: false,
35
+ adminMinInstances: 0,
36
+ adminMemory: ADMIN_MEMORY_DEFAULT,
37
+ gatewayMinInstances: 0,
38
+ wafAdaptiveProtection: false,
39
+ } as const;
40
+
41
+ export interface PulumiUpInputs {
42
+ installId: string;
43
+ installRoot: string;
44
+ secretsDir: string;
45
+ projectId: string;
46
+ domain: string;
47
+ /** #650 — the protected staging site's host (install setting). */
48
+ stagingDomain: string;
49
+ ownerEmail: string;
50
+ region: string;
51
+ /**
52
+ * #696 — the service account Pulumi's GCP provider impersonates
53
+ * (`caelo-provisioner@<project>`), with the operator's Application Default
54
+ * Credentials as the source. No key file. Null when this process runs AS
55
+ * the provisioner (the install job, install-job.ts): its ambient
56
+ * metadata-server credentials are the provisioner's own.
57
+ */
58
+ impersonateServiceAccount: string | null;
59
+ /** #696 — where the stack's state lives (state-backend.ts resolveBackend). */
60
+ backend: ResolvedBackend;
61
+ /**
62
+ * #696 — the install is deployed, so its stack must already be in
63
+ * `backend`; never create an empty one in its place.
64
+ */
65
+ stackMustExist?: boolean;
66
+ cloudSqlTier: string;
67
+ cloudSqlHa: boolean;
68
+ adminMinInstances: number;
69
+ /** #553 — admin memory (`2Gi`): it runs the Lighthouse quality audit. */
70
+ adminMemory: string;
71
+ gatewayMinInstances: number;
72
+ wafAdaptiveProtection: boolean;
73
+ iapAllowlist: string[];
74
+ /** Resolved sha256 digests per service (admin, gateway). The wizard
75
+ * pre-resolves the floating `:latest` release tag to a fixed digest
76
+ * so each pulumi up rolls Cloud Run to the newest release image. */
77
+ imageDigests: ImageDigests;
78
+ /**
79
+ * v0.3.1 — provider variant. Selects which stack dir to apply
80
+ * + which config namespace (caelo-gcp vs caelo-gcp-firebase).
81
+ * Defaults to 'gcp' for backwards compatibility.
82
+ */
83
+ provider?: "gcp" | "gcp-firebase";
84
+ }
85
+
86
+ export interface PulumiUpResult {
87
+ /**
88
+ * The stack outputs safe to checkpoint: no output Pulumi marks secret and
89
+ * none holding a credential (the Owner setup link). Code that needs a
90
+ * secret output reads it with {@link readStackOutput} when it needs it
91
+ * (#656).
92
+ */
93
+ outputs: Record<string, unknown>;
94
+ resourceCount: { created: number; updated: number; deleted: number };
95
+ }
96
+
97
+ /**
98
+ * Resolve the GCP stack workdir relative to the installed package.
99
+ * Works whether the CLI runs from source (apps/admin/scripts/...)
100
+ * or from the published npm tarball (node_modules/@caelo-cms/...).
101
+ */
102
+ export function gcpStackWorkDir(provider: "gcp" | "gcp-firebase" = "gcp"): string {
103
+ // The Pulumi.yaml + stacks/<provider>/index.ts ship in the npm
104
+ // tarball under `stacks/<provider>/`. From this file's location at
105
+ // runtime (dist/wizards/gcp-pulumi.js), the stack dir is two
106
+ // levels up + into the provider-specific stack folder.
107
+ return resolvePath(import.meta.dir, `../../stacks/${provider}`);
108
+ }
109
+
110
+ /**
111
+ * The stack config keys that pin each Cloud Run service's image
112
+ * (`<ns>:image-digest-<service>`, read by `imageRef` / `imageTag` in the
113
+ * stacks). Shared by the wizard and by `upgrade`'s write-back so both pin
114
+ * the same keys.
115
+ */
116
+ export function imageDigestConfig(
117
+ provider: "gcp" | "gcp-firebase",
118
+ digests: ImageDigests,
119
+ ): Record<string, { value: string }> {
120
+ return Object.fromEntries(
121
+ Object.entries(digests).map(([service, digest]) => [
122
+ `caelo-${provider}:image-digest-${service}`,
123
+ { value: digest },
124
+ ]),
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Write the digests `upgrade` rolled to into the install's Pulumi stack
130
+ * config, without running `pulumi up`, so a later `pulumi up` keeps them
131
+ * instead of re-resolving the floating tag. On `gcp`, an `image-<service>`
132
+ * override wins over the digest pin (stacks/gcp `imageTag`), so any such
133
+ * override is removed: it no longer describes what the service runs.
134
+ * Needs the `pulumi` CLI (the Automation API shells out to it); throws when
135
+ * the CLI or the stack is missing — the caller reports that.
136
+ *
137
+ * @returns the override keys it removed.
138
+ */
139
+ export async function writeImageDigestsToStack(inputs: {
140
+ backend: ResolvedBackend;
141
+ provider: "gcp" | "gcp-firebase";
142
+ digests: ImageDigests;
143
+ }): Promise<{ removedOverrides: string[] }> {
144
+ const stack = await selectInstallStack(inputs.backend, inputs.provider);
145
+ await stack.setAllConfig(imageDigestConfig(inputs.provider, inputs.digests));
146
+ const config = await stack.getAllConfig();
147
+ const removedOverrides = imageOverrideKeys(inputs.provider).filter((k) => k in config);
148
+ if (removedOverrides.length > 0) await stack.removeAllConfig(removedOverrides);
149
+ return { removedOverrides };
150
+ }
151
+
152
+ /**
153
+ * One output of the install's stack, secrets included (the Automation API's
154
+ * `pulumi stack output --show-secrets`). Read at the moment of use, never
155
+ * checkpointed (#656). Needs the `pulumi` CLI and the stack's state.
156
+ *
157
+ * @returns the output's value, or undefined when the stack has no such output
158
+ */
159
+ export async function readStackOutput(inputs: {
160
+ backend: ResolvedBackend;
161
+ provider: "gcp" | "gcp-firebase";
162
+ name: string;
163
+ }): Promise<unknown> {
164
+ const stack = await selectInstallStack(inputs.backend, inputs.provider);
165
+ const outputs = await stack.outputs();
166
+ return outputs[inputs.name]?.value;
167
+ }
168
+
169
+ /**
170
+ * The stack's settings file in the package's stack dir with secrets
171
+ * settings that can't contradict the install's state, or null when it is
172
+ * already fine (#696).
173
+ *
174
+ * That file (`Pulumi.production.yaml`) lives in the package, not with the
175
+ * state: every `bunx` version unpacks its own, and installs of the same
176
+ * provider on one machine share it, so it can name another install's
177
+ * passphrase salt or KMS key. A file naming anything but the install's own
178
+ * KMS key loses its secrets-provider fields and the `secure:` values
179
+ * encrypted under them (no stack config the CLI sets is secret);
180
+ * `PULUMI_FALLBACK_TO_STATE_SECRETS_MANAGER` (resolveBackend) then makes
181
+ * Pulumi take the provider — KMS key or passphrase salt — from the
182
+ * install's own state. A local install's salt can't be checked without
183
+ * reading its state, so any secrets settings in the file are dropped and
184
+ * re-read from the state on every run.
185
+ */
186
+ export function alignedStackSettings(
187
+ settings: pulumi.StackSettings,
188
+ backend: Pick<ResolvedBackend, "kind" | "secretsProvider">,
189
+ ): pulumi.StackSettings | null {
190
+ const hasSecretsSettings =
191
+ settings.secretsProvider !== undefined ||
192
+ settings.encryptedKey !== undefined ||
193
+ settings.encryptionSalt !== undefined;
194
+ const fine =
195
+ backend.kind === "gcs"
196
+ ? settings.secretsProvider === backend.secretsProvider &&
197
+ settings.encryptionSalt === undefined
198
+ : !hasSecretsSettings;
199
+ if (fine) return null;
200
+ const { secretsProvider: _p, encryptedKey: _k, encryptionSalt: _s, config, ...rest } = settings;
201
+ const kept = Object.fromEntries(
202
+ Object.entries(config ?? {}).filter(
203
+ ([, v]) => !(v !== null && typeof v === "object" && "secure" in (v as object)),
204
+ ),
205
+ );
206
+ return { ...rest, config: kept };
207
+ }
208
+
209
+ /**
210
+ * Bring the package's stack settings file in line with the install's backend
211
+ * ({@link alignedStackSettings}) before Pulumi reads it.
212
+ */
213
+ export async function alignStackSettingsFile(
214
+ backend: ResolvedBackend,
215
+ provider: "gcp" | "gcp-firebase",
216
+ ): Promise<void> {
217
+ const ws = await pulumi.LocalWorkspace.create({
218
+ workDir: gcpStackWorkDir(provider),
219
+ envVars: stateOnlyEnv(backend),
220
+ });
221
+ let settings: pulumi.StackSettings;
222
+ try {
223
+ settings = await ws.stackSettings(GCP_STACK_ENV);
224
+ } catch (e) {
225
+ if (/failed to find stack settings file/.test(String(e))) return;
226
+ throw e;
227
+ }
228
+ const aligned = alignedStackSettings(settings, backend);
229
+ if (aligned) await ws.saveStackSettings(GCP_STACK_ENV, aligned);
230
+ }
231
+
232
+ /**
233
+ * The environment for a Pulumi call that only touches the stack's state
234
+ * (select, config, outputs): the backend's variables, with ambient Google
235
+ * credentials blanked as in {@link pulumiGcpEnv} — reaching the ops bucket
236
+ * and KMS key must use the operator's ADC, the same as provisioning did,
237
+ * not a stray key file in the shell.
238
+ */
239
+ export function stateOnlyEnv(backend: Pick<ResolvedBackend, "envVars">): Record<string, string> {
240
+ return {
241
+ ...Object.fromEntries(AMBIENT_GOOGLE_CREDENTIAL_ENV.map((k) => [k, ""])),
242
+ ...backend.envVars,
243
+ };
244
+ }
245
+
246
+ /** The install's existing stack, on its backend. */
247
+ export async function selectInstallStack(
248
+ backend: ResolvedBackend,
249
+ provider: "gcp" | "gcp-firebase",
250
+ ): Promise<pulumi.Stack> {
251
+ await alignStackSettingsFile(backend, provider);
252
+ return pulumi.LocalWorkspace.selectStack(
253
+ { stackName: GCP_STACK_ENV, workDir: gcpStackWorkDir(provider) },
254
+ { envVars: stateOnlyEnv(backend) },
255
+ );
256
+ }
257
+
258
+ /**
259
+ * Stack config keys that override the image a service runs, ahead of the
260
+ * digest pin. Only the `gcp` stack reads them.
261
+ */
262
+ function imageOverrideKeys(provider: "gcp" | "gcp-firebase"): string[] {
263
+ return provider === "gcp" ? ["caelo-gcp:image-admin", "caelo-gcp:image-gateway"] : [];
264
+ }
265
+
266
+ /**
267
+ * Pick the stack's `secretReplication` (see stacks/<provider>/index.ts).
268
+ * A value already in config wins. Otherwise secrets that already exist
269
+ * in state with `auto` replication keep it — switching would replace
270
+ * them — and everything else (new stacks) gets `regional`.
271
+ */
272
+ export function resolveSecretReplication(
273
+ configured: string | undefined,
274
+ stateResources: ReadonlyArray<{ type?: string; outputs?: Record<string, unknown> }>,
275
+ ): "auto" | "regional" {
276
+ if (configured === "auto" || configured === "regional") return configured;
277
+ if (configured !== undefined) {
278
+ throw new Error(`secretReplication must be "auto" or "regional", got "${configured}"`);
279
+ }
280
+ const hasAutoSecret = stateResources.some((r) => {
281
+ if (r.type !== "gcp:secretmanager/secret:Secret") return false;
282
+ const replication = r.outputs?.replication as { auto?: unknown } | undefined;
283
+ return replication?.auto != null;
284
+ });
285
+ return hasAutoSecret ? "auto" : "regional";
286
+ }
287
+
288
+ /** A resource as `pulumi stack export` lists it. */
289
+ export interface StateResource {
290
+ type?: string;
291
+ inputs?: Record<string, unknown>;
292
+ outputs?: Record<string, unknown>;
293
+ }
294
+
295
+ /**
296
+ * The region an existing stack was deployed in, read from its state (the
297
+ * stacks' explicit `gcp.Provider` carries it), or null for a stack with
298
+ * nothing deployed. State, not stack config: the config file lives in the
299
+ * package's stack dir, which a newer `bunx` version doesn't share.
300
+ */
301
+ export function deployedStackRegion(resources: ReadonlyArray<StateResource>): string | null {
302
+ const regions = new Set(
303
+ resources
304
+ .filter((r) => r.type === "pulumi:providers:gcp")
305
+ .map((r) => r.inputs?.region)
306
+ .filter((v): v is string => typeof v === "string" && v.length > 0),
307
+ );
308
+ if (regions.size > 1) {
309
+ throw new Error(
310
+ `the Pulumi state holds GCP providers for several regions (${[...regions].join(", ")}); fix the stack before re-running.`,
311
+ );
312
+ }
313
+ return [...regions][0] ?? null;
314
+ }
315
+
316
+ /**
317
+ * Refuse to `up` a stack into another region than it was deployed in
318
+ * (#607): Pulumi would replace Cloud SQL, the buckets and the services.
319
+ */
320
+ export function assertStackRegion(deployed: string | null, requested: string): void {
321
+ if (deployed && deployed !== requested) {
322
+ throw new Error(
323
+ `this install's stack is deployed in ${deployed}, but the run asks for ${requested}. The region is fixed after install — moving it would replace the database, buckets and services. Set "region": "${deployed}" in install.json and re-run.`,
324
+ );
325
+ }
326
+ }
327
+
328
+ /** The inputs {@link applyStackConfig} sets as stack config. */
329
+ export type StackConfigInputs = Pick<
330
+ PulumiUpInputs,
331
+ | "projectId"
332
+ | "domain"
333
+ | "stagingDomain"
334
+ | "ownerEmail"
335
+ | "region"
336
+ | "cloudSqlTier"
337
+ | "cloudSqlHa"
338
+ | "adminMinInstances"
339
+ | "adminMemory"
340
+ | "gatewayMinInstances"
341
+ | "wafAdaptiveProtection"
342
+ | "iapAllowlist"
343
+ | "imageDigests"
344
+ > & { provider: "gcp" | "gcp-firebase" };
345
+
346
+ /**
347
+ * Set every config value the stack reads, then its `secretReplication`
348
+ * ({@link resolveSecretReplication}). The config file lives in the package's
349
+ * stack dir, so every run sets all of it — the wizard before `up`, and
350
+ * `state migrate` before the previews that prove a migration changed
351
+ * nothing (#696).
352
+ *
353
+ * @returns the secret replication now configured
354
+ */
355
+ export async function applyStackConfig(
356
+ stack: pulumi.Stack,
357
+ inputs: StackConfigInputs,
358
+ stateResources: ReadonlyArray<StateResource>,
359
+ ): Promise<"auto" | "regional"> {
360
+ const { provider } = inputs;
361
+ const ns = `caelo-${provider}`;
362
+ // wafAdaptiveProtection is gcp-only; the gcp-firebase stack has no
363
+ // Cloud Armor + ignores the key.
364
+ await stack.setAllConfig({
365
+ [`${ns}:project`]: { value: inputs.projectId },
366
+ [`${ns}:domain`]: { value: inputs.domain },
367
+ [`${ns}:stagingDomain`]: { value: inputs.stagingDomain },
368
+ [`${ns}:ownerEmail`]: { value: inputs.ownerEmail },
369
+ [`${ns}:region`]: { value: inputs.region },
370
+ [`${ns}:cloudSqlTier`]: { value: inputs.cloudSqlTier },
371
+ [`${ns}:cloudSqlHa`]: { value: String(inputs.cloudSqlHa) },
372
+ [`${ns}:adminMinInstances`]: { value: String(inputs.adminMinInstances) },
373
+ [`${ns}:adminMemory`]: { value: inputs.adminMemory },
374
+ [`${ns}:gatewayMinInstances`]: { value: String(inputs.gatewayMinInstances) },
375
+ ...(provider === "gcp"
376
+ ? { [`${ns}:wafAdaptiveProtection`]: { value: String(inputs.wafAdaptiveProtection) } }
377
+ : {}),
378
+ [`${ns}:iapAllowlist`]: { value: inputs.iapAllowlist.join(",") },
379
+ // v0.3.2 — anthropicApiKey config dropped. Runtime path is
380
+ // /settings/ai → ai_providers (KEK-encrypted).
381
+ ...imageDigestConfig(provider, inputs.imageDigests),
382
+ });
383
+
384
+ // Read back only after the required keys are set: `pulumi config` refuses to
385
+ // list a stack that lacks them, which is every fresh stack.
386
+ const configuredReplication = await stack.getConfig(`${ns}:secretReplication`).then(
387
+ (v) => v.value,
388
+ (e: unknown) => {
389
+ if (/not found/i.test(String(e))) return undefined;
390
+ throw e;
391
+ },
392
+ );
393
+ const secretReplication = resolveSecretReplication(configuredReplication, stateResources);
394
+ await stack.setConfig(`${ns}:secretReplication`, { value: secretReplication });
395
+ return secretReplication;
396
+ }
397
+
398
+ /**
399
+ * The environment `pulumi refresh` / `pulumi up` run with (#696).
400
+ *
401
+ * Pulumi's GCP provider impersonates the provisioner service account
402
+ * (`GOOGLE_IMPERSONATE_SERVICE_ACCOUNT`, read by the provider plugin itself,
403
+ * so it also applies to the stacks' explicit `gcp.Provider` and never enters
404
+ * the stack's state or its provider inputs). The source credential is the
405
+ * operator's Application Default Credentials — refreshable, so the provider
406
+ * re-mints the impersonated token whenever it expires and a run longer than
407
+ * an hour keeps working. Ambient credential variables are dropped so neither
408
+ * a key file (`GOOGLE_APPLICATION_CREDENTIALS`) nor a static token that dies
409
+ * after an hour can take the place of that chain. They are set to "" rather
410
+ * than left out: the Automation API (execa) merges this process's environment
411
+ * into the child's, and Google's libraries treat an empty value as unset. The
412
+ * backend (and, for a local install, its passphrase) are scoped to the child
413
+ * process only. Pulumi's CLI reaches an ops-bucket backend and its KMS key
414
+ * with the operator's ADC itself — the impersonation variable is read by the
415
+ * GCP provider plugin, not by the backend.
416
+ */
417
+ export function pulumiGcpEnv(
418
+ base: NodeJS.ProcessEnv,
419
+ inputs: Pick<PulumiUpInputs, "backend" | "impersonateServiceAccount">,
420
+ ): Record<string, string> {
421
+ return {
422
+ ...withoutAmbientGoogleCredentials(base),
423
+ ...Object.fromEntries(AMBIENT_GOOGLE_CREDENTIAL_ENV.map((k) => [k, ""])),
424
+ GOOGLE_IMPERSONATE_SERVICE_ACCOUNT: inputs.impersonateServiceAccount ?? "",
425
+ ...inputs.backend.envVars,
426
+ };
427
+ }
428
+
429
+ /**
430
+ * Run `pulumi up` against the GCP stack via the Automation SDK.
431
+ * Streams resource-create events to the supplied onEvent callback so
432
+ * the wizard can render a live progress bar.
433
+ */
434
+ export async function pulumiUpGcp(
435
+ inputs: PulumiUpInputs,
436
+ onEvent: (kind: "resource" | "error" | "log", message: string) => void,
437
+ ): Promise<PulumiUpResult> {
438
+ const env = pulumiGcpEnv(process.env, inputs);
439
+
440
+ const provider = inputs.provider ?? "gcp";
441
+ const stackName = GCP_STACK_ENV;
442
+ const workDir = gcpStackWorkDir(provider);
443
+
444
+ await alignStackSettingsFile(inputs.backend, provider);
445
+ const workspaceOpts = {
446
+ envVars: env,
447
+ // Used when the stack is created: a new install's secrets are
448
+ // encrypted with its own KMS key (or a local install's passphrase).
449
+ secretsProvider: inputs.backend.secretsProvider,
450
+ };
451
+ const stack = inputs.stackMustExist
452
+ ? await pulumi.LocalWorkspace.selectStack({ stackName, workDir }, workspaceOpts).catch(
453
+ (e: unknown) => {
454
+ throw new Error(
455
+ `the install is deployed but its stack is not in ${inputs.backend.url} (${String(e).split("\n")[0]}). Its state may have moved (state migrate) or been lost; nothing was changed.`,
456
+ );
457
+ },
458
+ )
459
+ : await pulumi.LocalWorkspace.createOrSelectStack({ stackName, workDir }, workspaceOpts);
460
+
461
+ const stateResources = ((await stack.exportStack()).deployment?.resources ??
462
+ []) as StateResource[];
463
+ assertStackRegion(deployedStackRegion(stateResources), inputs.region);
464
+
465
+ const secretReplication = await applyStackConfig(stack, { ...inputs, provider }, stateResources);
466
+
467
+ // The stack references the CLI-generated runtime secrets (stack-contract.ts
468
+ // CLI_GENERATED_SECRETS) by id, so they must exist before `up`.
469
+ const generated = await ensureGeneratedSecrets({
470
+ projectId: inputs.projectId,
471
+ env: stackName,
472
+ region: inputs.region,
473
+ replication: stackSecretReplication(secretReplication, inputs.region),
474
+ });
475
+ const notEnsured = generated.filter((o) => o.status === "failed");
476
+ if (notEnsured.length > 0) {
477
+ throw new Error(
478
+ `could not create the runtime secrets: ${notEnsured.map((o) => `${o.id}: ${o.error ?? ""}`).join("; ")}`,
479
+ );
480
+ }
481
+ for (const o of generated) {
482
+ if (o.detail) onEvent("log", `created ${o.id} (${o.detail})\n`);
483
+ }
484
+
485
+ // Refresh state first to detect drift from any out-of-band changes.
486
+ await stack.refresh({ onOutput: (msg) => onEvent("log", msg) });
487
+
488
+ // Run `up`. The onEvent callback receives the same JSON-RPC events
489
+ // Pulumi prints to stdout in --json mode.
490
+ const result = await stack.up({
491
+ onOutput: (msg) => onEvent("log", msg),
492
+ onEvent: (event) => {
493
+ // We only care about resource start/finish events for the
494
+ // progress UI — diagnostics surface via onOutput already.
495
+ if (event.resOpFailedEvent) {
496
+ onEvent(
497
+ "error",
498
+ `${event.resOpFailedEvent.metadata.urn}: ${event.resOpFailedEvent.status}`,
499
+ );
500
+ } else if (event.resOutputsEvent) {
501
+ onEvent("resource", event.resOutputsEvent.metadata.urn);
502
+ }
503
+ },
504
+ });
505
+
506
+ const summary = result.summary;
507
+ const changes = summary.resourceChanges ?? {};
508
+ return {
509
+ outputs: checkpointableOutputs(result.outputs),
510
+ resourceCount: {
511
+ created: changes.create ?? 0,
512
+ updated: changes.update ?? 0,
513
+ deleted: changes.delete ?? 0,
514
+ },
515
+ };
516
+ }