@intentius/chant-lexicon-terraform 0.124.51 → 0.124.54

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 (338) hide show
  1. package/README.md +7 -6
  2. package/dist/__fixtures__/pr-loop/estate.component.d.ts +7 -0
  3. package/dist/__fixtures__/pr-loop/estate.component.d.ts.map +1 -0
  4. package/dist/__fixtures__/pr-loop/estate.component.js +7 -0
  5. package/dist/__fixtures__/report/generate.d.ts +26 -0
  6. package/dist/__fixtures__/report/generate.d.ts.map +1 -0
  7. package/dist/__fixtures__/report/generate.js +58 -0
  8. package/dist/applied-state.d.ts +27 -0
  9. package/dist/applied-state.d.ts.map +1 -0
  10. package/dist/applied-state.js +44 -0
  11. package/dist/backend-config.d.ts +53 -0
  12. package/dist/backend-config.d.ts.map +1 -0
  13. package/dist/backend-config.js +136 -0
  14. package/dist/behaviour/kinds.d.ts.map +1 -1
  15. package/dist/behaviour/kinds.js +4 -0
  16. package/dist/behaviour/predict.d.ts.map +1 -1
  17. package/dist/behaviour/predict.js +6 -2
  18. package/dist/behaviour/reference-catalog.d.ts +16 -5
  19. package/dist/behaviour/reference-catalog.d.ts.map +1 -1
  20. package/dist/behaviour/reference-catalog.js +16 -5
  21. package/dist/behaviour/request.d.ts.map +1 -1
  22. package/dist/behaviour/request.js +3 -2
  23. package/dist/changed-roots.d.ts +6 -4
  24. package/dist/changed-roots.d.ts.map +1 -1
  25. package/dist/changed-roots.js +14 -18
  26. package/dist/commands.d.ts +5 -0
  27. package/dist/commands.d.ts.map +1 -1
  28. package/dist/commands.js +87 -4
  29. package/dist/components/capability-plugin.d.ts +65 -3
  30. package/dist/components/capability-plugin.d.ts.map +1 -1
  31. package/dist/components/capability-plugin.js +197 -13
  32. package/dist/components/compose-group.d.ts +145 -0
  33. package/dist/components/compose-group.d.ts.map +1 -0
  34. package/dist/components/compose-group.js +407 -0
  35. package/dist/components/placeholder-preview.d.ts +78 -0
  36. package/dist/components/placeholder-preview.d.ts.map +1 -0
  37. package/dist/components/placeholder-preview.js +254 -0
  38. package/dist/compose/apply-events.d.ts +46 -0
  39. package/dist/compose/apply-events.d.ts.map +1 -0
  40. package/dist/compose/apply-events.js +77 -0
  41. package/dist/compose/estate.d.ts +194 -0
  42. package/dist/compose/estate.d.ts.map +1 -0
  43. package/dist/compose/estate.js +363 -0
  44. package/dist/compose/hcl.d.ts +99 -0
  45. package/dist/compose/hcl.d.ts.map +1 -0
  46. package/dist/compose/hcl.js +302 -0
  47. package/dist/compose/module-address.d.ts +24 -0
  48. package/dist/compose/module-address.d.ts.map +1 -0
  49. package/dist/compose/module-address.js +43 -0
  50. package/dist/compose/root.d.ts +127 -0
  51. package/dist/compose/root.d.ts.map +1 -0
  52. package/dist/compose/root.js +586 -0
  53. package/dist/compose/run.d.ts +183 -0
  54. package/dist/compose/run.d.ts.map +1 -0
  55. package/dist/compose/run.js +447 -0
  56. package/dist/compose/slice.d.ts +102 -0
  57. package/dist/compose/slice.d.ts.map +1 -0
  58. package/dist/compose/slice.js +249 -0
  59. package/dist/compose/state-address.d.ts +28 -0
  60. package/dist/compose/state-address.d.ts.map +1 -0
  61. package/dist/compose/state-address.js +154 -0
  62. package/dist/compose/state-merge.d.ts +52 -0
  63. package/dist/compose/state-merge.d.ts.map +1 -0
  64. package/dist/compose/state-merge.js +135 -0
  65. package/dist/compose.d.ts +179 -0
  66. package/dist/compose.d.ts.map +1 -0
  67. package/dist/compose.js +482 -0
  68. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  69. package/dist/composites/terraform-apply-op.js +7 -3
  70. package/dist/config.d.ts +59 -13
  71. package/dist/config.d.ts.map +1 -1
  72. package/dist/config.js +43 -9
  73. package/dist/describe-resources.d.ts +74 -12
  74. package/dist/describe-resources.d.ts.map +1 -1
  75. package/dist/describe-resources.js +374 -66
  76. package/dist/drift-read.d.ts +33 -0
  77. package/dist/drift-read.d.ts.map +1 -0
  78. package/dist/drift-read.js +243 -0
  79. package/dist/drift-record.d.ts +147 -0
  80. package/dist/drift-record.d.ts.map +1 -0
  81. package/dist/drift-record.js +383 -0
  82. package/dist/hcl/edges.d.ts.map +1 -1
  83. package/dist/hcl/edges.js +24 -1
  84. package/dist/hcl/files.d.ts +61 -0
  85. package/dist/hcl/files.d.ts.map +1 -0
  86. package/dist/hcl/files.js +221 -0
  87. package/dist/hcl/instances.d.ts +82 -0
  88. package/dist/hcl/instances.d.ts.map +1 -0
  89. package/dist/hcl/instances.js +390 -0
  90. package/dist/hcl/parse.d.ts +81 -9
  91. package/dist/hcl/parse.d.ts.map +1 -1
  92. package/dist/hcl/parse.js +114 -16
  93. package/dist/hcl/roots.d.ts.map +1 -1
  94. package/dist/hcl/roots.js +5 -0
  95. package/dist/hcl/suppressions.d.ts.map +1 -1
  96. package/dist/hcl/suppressions.js +8 -2
  97. package/dist/index.d.ts +11 -2
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +21 -2
  100. package/dist/integrity.json +3 -3
  101. package/dist/live-plan-reasons.d.ts +21 -0
  102. package/dist/live-plan-reasons.d.ts.map +1 -0
  103. package/dist/live-plan-reasons.js +47 -0
  104. package/dist/lsp/option-keys.d.ts.map +1 -1
  105. package/dist/lsp/option-keys.js +2 -0
  106. package/dist/manifest.json +1 -1
  107. package/dist/op/activities/index.d.ts +6 -2
  108. package/dist/op/activities/index.d.ts.map +1 -1
  109. package/dist/op/activities/index.js +5 -1
  110. package/dist/op/activities/live-detect.d.ts +4 -3
  111. package/dist/op/activities/live-detect.d.ts.map +1 -1
  112. package/dist/op/activities/live-detect.js +57 -32
  113. package/dist/op/activities/pin-rollout.d.ts +2 -0
  114. package/dist/op/activities/pin-rollout.d.ts.map +1 -1
  115. package/dist/op/activities/pin-rollout.js +1 -0
  116. package/dist/op/activities/terraform.d.ts +153 -5
  117. package/dist/op/activities/terraform.d.ts.map +1 -1
  118. package/dist/op/activities/terraform.js +205 -20
  119. package/dist/op/activity-contracts.d.ts +27 -0
  120. package/dist/op/activity-contracts.d.ts.map +1 -1
  121. package/dist/op/activity-contracts.js +19 -1
  122. package/dist/op/builders.d.ts +7 -1
  123. package/dist/op/builders.d.ts.map +1 -1
  124. package/dist/op/builders.js +9 -0
  125. package/dist/pin/edit.d.ts.map +1 -1
  126. package/dist/pin/edit.js +35 -0
  127. package/dist/pin/rollout.d.ts +2 -0
  128. package/dist/pin/rollout.d.ts.map +1 -1
  129. package/dist/pin/rollout.js +14 -11
  130. package/dist/plan-shape.d.ts +30 -0
  131. package/dist/plan-shape.d.ts.map +1 -0
  132. package/dist/plan-shape.js +125 -0
  133. package/dist/plugin.d.ts.map +1 -1
  134. package/dist/plugin.js +32 -18
  135. package/dist/report.d.ts +216 -0
  136. package/dist/report.d.ts.map +1 -0
  137. package/dist/report.js +666 -0
  138. package/dist/skills/chant-terraform.md +1 -1
  139. package/dist/state/azure-blob.d.ts +68 -0
  140. package/dist/state/azure-blob.d.ts.map +1 -0
  141. package/dist/state/azure-blob.js +201 -0
  142. package/dist/state/backend.d.ts +158 -0
  143. package/dist/state/backend.d.ts.map +1 -0
  144. package/dist/state/backend.js +406 -0
  145. package/dist/state/exec.d.ts +39 -0
  146. package/dist/state/exec.d.ts.map +1 -0
  147. package/dist/state/exec.js +80 -0
  148. package/dist/state/gcs.d.ts +62 -0
  149. package/dist/state/gcs.d.ts.map +1 -0
  150. package/dist/state/gcs.js +224 -0
  151. package/dist/state/index.d.ts +15 -0
  152. package/dist/state/index.d.ts.map +1 -0
  153. package/dist/state/index.js +14 -0
  154. package/dist/state/locks.d.ts +105 -0
  155. package/dist/state/locks.d.ts.map +1 -0
  156. package/dist/state/locks.js +130 -0
  157. package/dist/state/object-store.d.ts +37 -0
  158. package/dist/state/object-store.d.ts.map +1 -0
  159. package/dist/state/object-store.js +20 -0
  160. package/dist/state/root-states.d.ts +131 -0
  161. package/dist/state/root-states.d.ts.map +1 -0
  162. package/dist/state/root-states.js +241 -0
  163. package/dist/state/s3.d.ts +68 -0
  164. package/dist/state/s3.d.ts.map +1 -0
  165. package/dist/state/s3.js +170 -0
  166. package/dist/state/store.d.ts +51 -0
  167. package/dist/state/store.d.ts.map +1 -0
  168. package/dist/state/store.js +185 -0
  169. package/dist/terragrunt/affected.d.ts.map +1 -1
  170. package/dist/terragrunt/affected.js +2 -1
  171. package/package.json +7 -3
  172. package/src/__fixtures__/ACCEPTANCE.md +69 -7
  173. package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
  174. package/src/__fixtures__/compose/state-app.json +66 -0
  175. package/src/__fixtures__/compose/state-edge.json +40 -0
  176. package/src/__fixtures__/compose/state-net.json +58 -0
  177. package/src/__fixtures__/compose/three-app.plan.json +184 -0
  178. package/src/__fixtures__/compose/three-composed.plan.json +509 -0
  179. package/src/__fixtures__/compose/three-edge.plan.json +144 -0
  180. package/src/__fixtures__/compose/three-net.plan.json +174 -0
  181. package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
  182. package/src/__fixtures__/compose/working-after-apply.json +145 -0
  183. package/src/__fixtures__/config-blocks/app/main.tf +61 -0
  184. package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
  185. package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
  186. package/src/__fixtures__/config-files/shadow/main.tf +3 -0
  187. package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
  188. package/src/__fixtures__/config-files/shadow/other.tf +3 -0
  189. package/src/__fixtures__/config-files/tf/main.tf +11 -0
  190. package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
  191. package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
  192. package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
  193. package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
  194. package/src/__fixtures__/drift-record/README.md +11 -0
  195. package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
  196. package/src/__fixtures__/instance-forms/app/main.tf +110 -0
  197. package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
  198. package/src/__fixtures__/instance-vars/app/main.tf +52 -0
  199. package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
  200. package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
  201. package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
  202. package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
  203. package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
  204. package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
  205. package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
  206. package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
  207. package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
  208. package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
  209. package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
  210. package/src/__fixtures__/pr-loop/README.md +50 -0
  211. package/src/__fixtures__/pr-loop/chant.config.json +13 -0
  212. package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
  213. package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
  214. package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
  215. package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
  216. package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
  217. package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
  218. package/src/__fixtures__/report/applied.show.json +51 -0
  219. package/src/__fixtures__/report/generate.ts +63 -0
  220. package/src/__fixtures__/report/plan.show.json +200 -0
  221. package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
  222. package/src/__fixtures__/report/tf-apply/report.json +338 -0
  223. package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
  224. package/src/__fixtures__/report/tf-plan/report.json +320 -0
  225. package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
  226. package/src/applied-state.ts +52 -0
  227. package/src/backend-config.test.ts +105 -0
  228. package/src/backend-config.ts +133 -0
  229. package/src/behaviour/kinds.ts +4 -0
  230. package/src/behaviour/predict.ts +5 -2
  231. package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
  232. package/src/behaviour/reference-catalog.ts +16 -5
  233. package/src/behaviour/request.ts +3 -2
  234. package/src/change-set-instances.test.ts +98 -0
  235. package/src/changed-roots.ts +14 -16
  236. package/src/commands.ts +84 -4
  237. package/src/components/capability-plugin.test.ts +172 -5
  238. package/src/components/capability-plugin.ts +212 -11
  239. package/src/components/change-set-graph.e2e.test.ts +11 -5
  240. package/src/components/compose-group.test.ts +486 -0
  241. package/src/components/compose-group.ts +489 -0
  242. package/src/components/demo-composed-roots.e2e.test.ts +440 -0
  243. package/src/components/member-outputs.test.ts +76 -0
  244. package/src/components/placeholder-preview.test.ts +147 -0
  245. package/src/components/placeholder-preview.ts +257 -0
  246. package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
  247. package/src/components/pr-loop.tofu.test.ts +112 -56
  248. package/src/compose/apply-events.test.ts +62 -0
  249. package/src/compose/apply-events.ts +100 -0
  250. package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
  251. package/src/compose/estate.test.ts +421 -0
  252. package/src/compose/estate.ts +443 -0
  253. package/src/compose/hcl.ts +311 -0
  254. package/src/compose/module-address.ts +49 -0
  255. package/src/compose/root.test.ts +416 -0
  256. package/src/compose/root.ts +592 -0
  257. package/src/compose/run.test.ts +440 -0
  258. package/src/compose/run.tofu.test.ts +176 -0
  259. package/src/compose/run.ts +527 -0
  260. package/src/compose/slice.test.ts +184 -0
  261. package/src/compose/slice.ts +290 -0
  262. package/src/compose/state-address.ts +157 -0
  263. package/src/compose/state-merge.test.ts +131 -0
  264. package/src/compose/state-merge.ts +148 -0
  265. package/src/compose.test.ts +248 -0
  266. package/src/compose.ts +627 -0
  267. package/src/composites/terraform-apply-op.test.ts +9 -0
  268. package/src/composites/terraform-apply-op.ts +7 -3
  269. package/src/composites/terraform-pin-rollout-op.test.ts +2 -2
  270. package/src/config-file-listers.test.ts +265 -0
  271. package/src/config.ts +44 -9
  272. package/src/describe-resources.engine-facts.test.ts +412 -0
  273. package/src/describe-resources.live-module-keyed.test.ts +290 -0
  274. package/src/describe-resources.ts +416 -70
  275. package/src/drift-read.ts +284 -0
  276. package/src/drift-record.test.ts +223 -0
  277. package/src/drift-record.ts +455 -0
  278. package/src/hcl/config-blocks.test.ts +174 -0
  279. package/src/hcl/edges.ts +21 -1
  280. package/src/hcl/files.ts +234 -0
  281. package/src/hcl/instance-forms.test.ts +94 -0
  282. package/src/hcl/instances.test.ts +137 -0
  283. package/src/hcl/instances.ts +422 -0
  284. package/src/hcl/parse.test.ts +139 -1
  285. package/src/hcl/parse.ts +149 -16
  286. package/src/hcl/roots.ts +6 -0
  287. package/src/hcl/suppressions.ts +8 -2
  288. package/src/index.ts +76 -0
  289. package/src/live-plan-reasons.ts +54 -0
  290. package/src/lsp/completions.test.ts +1 -1
  291. package/src/lsp/option-keys.ts +2 -0
  292. package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
  293. package/src/op/activities/index.ts +9 -0
  294. package/src/op/activities/live-detect.ts +55 -26
  295. package/src/op/activities/pin-rollout.ts +3 -0
  296. package/src/op/activities/registry.test.ts +2 -1
  297. package/src/op/activities/terraform.test.ts +55 -0
  298. package/src/op/activities/terraform.ts +266 -22
  299. package/src/op/activity-contracts.test.ts +1 -0
  300. package/src/op/activity-contracts.ts +24 -1
  301. package/src/op/builders.ts +14 -0
  302. package/src/pin/bundle.test.ts +2 -0
  303. package/src/pin/edit.ts +30 -0
  304. package/src/pin/rollout.ts +16 -11
  305. package/src/plan-shape.test.ts +148 -0
  306. package/src/plan-shape.ts +128 -0
  307. package/src/plugin.ts +33 -19
  308. package/src/report.test.ts +423 -0
  309. package/src/report.ts +752 -0
  310. package/src/signal-only-root.test.ts +72 -0
  311. package/src/skills/chant-terraform.md +1 -1
  312. package/src/state/azure-blob.ts +223 -0
  313. package/src/state/backend-verdict.test.ts +187 -0
  314. package/src/state/backend.test.ts +127 -0
  315. package/src/state/backend.ts +488 -0
  316. package/src/state/exec.ts +111 -0
  317. package/src/state/gcs.ts +254 -0
  318. package/src/state/index.ts +15 -0
  319. package/src/state/locks.test.ts +84 -0
  320. package/src/state/locks.ts +201 -0
  321. package/src/state/object-store.ts +35 -0
  322. package/src/state/root-states-floci.e2e.test.ts +66 -0
  323. package/src/state/root-states.test.ts +462 -0
  324. package/src/state/root-states.ts +309 -0
  325. package/src/state/s3.ts +193 -0
  326. package/src/state/store.ts +225 -0
  327. package/src/terragrunt/affected.ts +2 -1
  328. package/src/workspace-kinds.test.ts +4 -17
  329. package/workspace-kinds.json +1 -24
  330. package/dist/workspace-member.d.ts +0 -30
  331. package/dist/workspace-member.d.ts.map +0 -1
  332. package/dist/workspace-member.js +0 -91
  333. package/schemas/pulumi.schema.json +0 -34
  334. package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
  335. package/src/testdata/pulumi-member/stack-export.json +0 -53
  336. package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
  337. package/src/workspace-member.test.ts +0 -89
  338. package/src/workspace-member.ts +0 -126
@@ -0,0 +1,443 @@
1
+ /**
2
+ * The composed run of choudoufu estates (chant#4445, epic #4419): when every
3
+ * member runs choudoufu against a `live` estate (`Composition.estate`), one
4
+ * `choudoufu plan` and one `choudoufu apply` from a composed root of `module`
5
+ * calls (choudoufu#2059).
6
+ *
7
+ * An estate has no state file, so nothing is pulled, locked, split or
8
+ * written back: each resource's record goes to its own estate as it applies,
9
+ * and a `terraform_estate_outputs` read of another estate in the run is
10
+ * answered inside the run. The composed root is `writeEstateRoot`'s
11
+ * (./root.ts, #4441). The run takes the job's environment and assumes no
12
+ * role of its own: each estate's provider blocks name its role.
13
+ *
14
+ * choudoufu refuses a bare `show` under a `live` block, so the values the
15
+ * plan left unknown are read after the apply from a second plan of the
16
+ * composed root: its `prior_state`, sliced per module (./slice.ts, #4442) and
17
+ * read through `../applied-state`, the reader `chant terraform report` uses.
18
+ * The same plan says how far a failed apply got: a member whose part plans
19
+ * no change any more applied.
20
+ *
21
+ * Lifted from terragucci's `packages/terragucci/src/compose.ts` at c493e0e
22
+ * (`ComposedRun` with `estate` set: `plan`, `apply`, `finishEstate`,
23
+ * `showEstateMembers`, `composedEnv`). The state-backed run is #4444; this one
24
+ * has the same `plan()`, `apply()` and `release()` and returns the same
25
+ * shapes, and #4447 picks one by `Composition.estate`.
26
+ */
27
+
28
+ import { existsSync, mkdtempSync, readdirSync, rmdirSync, rmSync } from "node:fs";
29
+ import { tmpdir } from "node:os";
30
+ import { join, resolve } from "node:path";
31
+ import type { CapabilityGroupOutcome, CapabilityGroupPlan, GroupMemberOutcome } from "@intentius/chant/components/capability";
32
+ import type { ComposeFallback, Composition } from "../compose";
33
+ import type { TerraformConfig } from "../config";
34
+ import { stateOutputs, stateResources, type StateResourceValues } from "../applied-state";
35
+ import { terraformPlanDigest } from "../plan-digest";
36
+ import { terraformPlannedOutputs } from "../components/capability-plugin";
37
+ import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
38
+ import { isOlderVersion, MIN_CHOUDOUFU_ESTATES_VERSION, parseChoudoufuVersion } from "../op/activities/terraform";
39
+ import { runBinary, type BinaryExec } from "../state/exec";
40
+ import { memberApply, type ApplyEvents, type MemberApply } from "./apply-events";
41
+ import { liveEstate } from "./hcl";
42
+ import { stripModule, topModule } from "./module-address";
43
+ import { COMPOSE_DIR, withoutVarFiles, writeEstateRoot, type ComposedRoot } from "./root";
44
+ import { composedMemberPlans, planCounts, slicePlan, sliceText, type ComposedMemberPlan, type PlanCounts } from "./slice";
45
+
46
+ /** What a composed run's `plan()` answers. Assignable to core's `CapabilityGroupPlan` (ws-107). */
47
+ export interface ComposedRunPlan extends CapabilityGroupPlan {
48
+ /** Each member's part of the plan, by component. Empty when the run fell back or the plan failed. */
49
+ members: Record<string, ComposedMemberPlan>;
50
+ /** What the approval binds beside the members' own plans: the composed plan and the generated files. Empty when nothing planned. */
51
+ digest: string;
52
+ /** Why the run did not plan as one: the members plan wave by wave instead. Decided before anything planned. */
53
+ fallback?: ComposeFallback;
54
+ /** Why the composed plan failed: every member is plan-failed. */
55
+ error?: string;
56
+ /** The member {@link error} is about, when it is about one (a hook, an init, a lock). */
57
+ member?: string;
58
+ /** The composed plan's `show -json`. */
59
+ plan?: unknown;
60
+ /** The saved plan file `apply()` applies. */
61
+ planFile: string;
62
+ /** The composed plan's `show` text. */
63
+ text: string;
64
+ /** Each member's part of the `show` text, by component. */
65
+ texts: Record<string, string>;
66
+ /** sha256 of each generated file of the composed root, as `ComposedRoot.generated`. */
67
+ generated: [string, string][];
68
+ /** The whole run's counts. */
69
+ counts: PlanCounts;
70
+ /** The binary's `Plan:` line for the whole run. */
71
+ summary: string;
72
+ }
73
+
74
+ /** One member's outcome of a composed apply. Assignable to core's `GroupMemberOutcome` (ws-107). */
75
+ export interface ComposedMemberOutcome extends GroupMemberOutcome {
76
+ status: MemberApply;
77
+ /** Its planned changes. */
78
+ planned: number;
79
+ /** Its planned changes that completed. */
80
+ done: number;
81
+ /** Each resource of its part of the state after the apply, by address with its module taken off. */
82
+ resources?: Map<string, StateResourceValues>;
83
+ }
84
+
85
+ /** What a composed run's `apply()` answers. Assignable to core's `CapabilityGroupOutcome` (ws-107). */
86
+ export interface ComposedRunApply extends CapabilityGroupOutcome {
87
+ /** Every member applied or had no changes, and the binary exited 0. */
88
+ ok: boolean;
89
+ members: Record<string, ComposedMemberOutcome>;
90
+ /** `show -json` of the plan made after the apply. Absent when it could not be made. */
91
+ after?: unknown;
92
+ }
93
+
94
+ /** The surface #4447 drives, whichever run `Composition.estate` picks. */
95
+ export interface ComposedRunner {
96
+ plan(): Promise<ComposedRunPlan>;
97
+ apply(): Promise<ComposedRunApply>;
98
+ release(): Promise<void>;
99
+ }
100
+
101
+ export interface EstateRunOptions {
102
+ /** The checkout's root: where `chant.config.*` was loaded from. `.chant-compose/` is written here. */
103
+ projectRoot: string;
104
+ /** What `compositionFor` answered, with `estate` set and two or more members. */
105
+ composition: Composition;
106
+ /** The config's `terraform` namespace: a root's `estate`. */
107
+ terraform: TerraformConfig;
108
+ /** The job's environment. The run adds no role to it. */
109
+ env: NodeJS.ProcessEnv;
110
+ /** The choudoufu binary, by name or path. Default `terraform.binary`, else `choudoufu`. */
111
+ binary?: string;
112
+ /** Where the plan files go. Default: a new directory under the system's temp directory. */
113
+ work?: string;
114
+ /** How the binary runs. Default {@link runBinary}; a test injects a fake. */
115
+ exec?: BinaryExec;
116
+ log?: (line: string) => void;
117
+ }
118
+
119
+ const firstLines = (s: string, n = 20): string => s.trim().split("\n").slice(-n).join("\n");
120
+
121
+ /**
122
+ * Why a choudoufu's `version` output cannot run a composed run of estates, or
123
+ * undefined when it can. A release older than
124
+ * {@link MIN_CHOUDOUFU_ESTATES_VERSION} falls back; a dev build names no
125
+ * release and passes.
126
+ */
127
+ export function estateVersionFallback(versionOutput: string): ComposeFallback | undefined {
128
+ const version = parseChoudoufuVersion(versionOutput);
129
+ if (version === undefined || !isOlderVersion(version, MIN_CHOUDOUFU_ESTATES_VERSION)) return undefined;
130
+ return {
131
+ kind: "binary",
132
+ reason: `choudoufu ${version} is older than v${MIN_CHOUDOUFU_ESTATES_VERSION}, the first release that plans and applies several estates as one (choudoufu#2059)`,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * What choudoufu's human `apply` output says of each module: the changes
138
+ * that completed (`<addr>: Creation complete`, `Modifications complete`,
139
+ * `Destruction complete`), started (`Creating...` and the rest) or errored
140
+ * (an `Error:` box naming `with <addr>,`). An estate's apply runs under live
141
+ * resource markers and prints no `-json` lines, so this reads the text.
142
+ */
143
+ export function readEstateApplyText(out: string, modules: readonly string[]): ApplyEvents {
144
+ const ev: ApplyEvents = { done: new Map(), started: new Map(), errored: new Map(), other: [] };
145
+ const known = new Set(modules);
146
+ const moduleOf = (addr: string): string | undefined => {
147
+ const m = topModule(addr);
148
+ return m !== undefined && known.has(m) && addr !== `module.${m}` ? m : undefined;
149
+ };
150
+ const bump = (map: Map<string, number>, m: string): void => {
151
+ map.set(m, (map.get(m) ?? 0) + 1);
152
+ };
153
+ const lines = out.split("\n").map((l) => l.replace(/^[│╷╵\s]+/, "").trimEnd());
154
+ for (let i = 0; i < lines.length; i++) {
155
+ const l = lines[i]!;
156
+ const hit = l.match(/^(module\.\S+): (Creation|Modifications|Destruction) complete\b/);
157
+ if (hit) {
158
+ const m = moduleOf(hit[1]!);
159
+ if (m) bump(ev.done, m);
160
+ continue;
161
+ }
162
+ const start = l.match(/^(module\.\S+): (Creating|Modifying|Destroying)\.\.\./);
163
+ if (start) {
164
+ const m = moduleOf(start[1]!);
165
+ if (m) bump(ev.started, m);
166
+ continue;
167
+ }
168
+ if (l.startsWith("Error: ")) {
169
+ let addr: string | undefined;
170
+ for (let j = i + 1; j < Math.min(lines.length, i + 8) && !lines[j]!.startsWith("Error: "); j++) {
171
+ const w = lines[j]!.match(/^with (module\.[^,\s]+),?$/);
172
+ if (w) {
173
+ addr = w[1]!;
174
+ break;
175
+ }
176
+ }
177
+ const m = addr ? moduleOf(addr) : undefined;
178
+ if (m && !ev.errored.has(m)) ev.errored.set(m, `${stripModule(addr!)}: ${l}`);
179
+ else if (!m) ev.other.push(l);
180
+ }
181
+ }
182
+ return ev;
183
+ }
184
+
185
+ /**
186
+ * Each member's outcome from its planned changes, what the apply printed, its
187
+ * exit code and, when the post-apply plan was made, the changes each member's
188
+ * part still plans. A clean exit means every planned change applied. After a
189
+ * failure, a member's changes left in the post-apply plan say how many of its
190
+ * planned changes applied; without that plan the printed completions do.
191
+ */
192
+ export function estateOutcomes(
193
+ c: Pick<Composition, "order" | "modules">,
194
+ planned: ReadonlyMap<string, number>,
195
+ out: string,
196
+ code: number,
197
+ remaining?: ReadonlyMap<string, number>,
198
+ ): { members: Record<string, { status: MemberApply; done: number; planned: number; error?: string }>; other: string[] } {
199
+ const ev = readEstateApplyText(out, c.order.map((member) => c.modules.get(member)!));
200
+ for (const member of c.order) {
201
+ const m = c.modules.get(member)!;
202
+ const n = planned.get(member) ?? 0;
203
+ if (code === 0) ev.done.set(m, n);
204
+ else if (remaining?.has(member)) ev.done.set(m, Math.max(0, n - remaining.get(member)!));
205
+ }
206
+ const members: Record<string, { status: MemberApply; done: number; planned: number; error?: string }> = {};
207
+ for (const member of c.order) {
208
+ const m = c.modules.get(member)!;
209
+ const n = planned.get(member) ?? 0;
210
+ const { apply, done } = memberApply(m, n, ev);
211
+ const error = ev.errored.get(m);
212
+ members[member] = { status: apply, done, planned: n, ...(error && apply !== "applied" && apply !== "no-changes" ? { error } : {}) };
213
+ }
214
+ return { members, other: ev.other };
215
+ }
216
+
217
+ /**
218
+ * One composed run of estates: `plan()`, then `apply()` once the approval is
219
+ * given, then `release()` whatever happened. No call reads, locks or writes
220
+ * a state.
221
+ */
222
+ export class EstateRun implements ComposedRunner {
223
+ private readonly exec: BinaryExec;
224
+ private readonly binary: string;
225
+ private readonly composeDir: string;
226
+ private readonly work: string;
227
+ private readonly madeWork: boolean;
228
+ private planned?: ComposedRunPlan;
229
+ private root?: ComposedRoot;
230
+
231
+ constructor(private readonly o: EstateRunOptions) {
232
+ const c = o.composition;
233
+ if (!c.estate || c.fallback || c.members.length < 2) throw new Error(`EstateRun runs a composition of estates with two or more members${c.fallback ? `; this one fell back: ${c.fallback.reason}` : ""}`);
234
+ this.exec = o.exec ?? runBinary;
235
+ this.binary = o.binary ?? o.terraform.binary ?? "choudoufu";
236
+ this.composeDir = join(resolve(o.projectRoot), COMPOSE_DIR);
237
+ this.madeWork = o.work === undefined;
238
+ this.work = o.work ?? mkdtempSync(join(tmpdir(), "chant-compose-estate-"));
239
+ }
240
+
241
+ private get c(): Composition {
242
+ return this.o.composition;
243
+ }
244
+
245
+ private log(line: string): void {
246
+ this.o.log?.(line);
247
+ }
248
+
249
+ /**
250
+ * The composed root's environment: the job's, with no role added (each
251
+ * estate's provider blocks name its role) and its `-var-file` flags taken
252
+ * out. With a shared provider cache and no lock file to check a cached
253
+ * provider against, the cache may fill the lock file.
254
+ */
255
+ private env(): NodeJS.ProcessEnv {
256
+ const env = withoutVarFiles(this.o.env);
257
+ return env.TF_PLUGIN_CACHE_DIR && !existsSync(join(this.composeDir, ".terraform.lock.hcl")) ? { ...env, TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE: "true" } : env;
258
+ }
259
+
260
+ /** The estate each member's records go to: its root's `estate`, else its `live` block's. */
261
+ private estateOf(member: string): string | undefined {
262
+ const key = this.c.roots.get(member);
263
+ return (key !== undefined ? this.o.terraform.roots[key]?.estate : undefined) ?? liveEstate(this.c.dirs.get(member)!, this.binary).estate;
264
+ }
265
+
266
+ private notPlanned(extra: { fallback?: ComposeFallback; error?: string }): ComposedRunPlan {
267
+ return {
268
+ members: {},
269
+ digest: "",
270
+ planFile: join(this.work, "composed.tfplan"),
271
+ text: "",
272
+ texts: {},
273
+ generated: [],
274
+ counts: { add: 0, change: 0, destroy: 0, changes: 0, destroys: 0 },
275
+ summary: "",
276
+ ...extra,
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Why this choudoufu cannot run the composition, or undefined. Runs
282
+ * `choudoufu version`; what #4447's `groupFallback` asks before planning.
283
+ */
284
+ async fallback(): Promise<ComposeFallback | undefined> {
285
+ const v = await this.exec(this.binary, ["version"], resolve(this.o.projectRoot), this.o.env);
286
+ // A binary that cannot say its version is left to the plan, which says why it fails.
287
+ return v.code === 0 ? estateVersionFallback(v.stdout) : undefined;
288
+ }
289
+
290
+ private writeRoot(): ComposedRoot {
291
+ this.root = writeEstateRoot({ projectRoot: this.o.projectRoot, composition: this.c });
292
+ return this.root;
293
+ }
294
+
295
+ private restoreRoot(): void {
296
+ const r = this.root;
297
+ this.root = undefined;
298
+ r?.restore();
299
+ }
300
+
301
+ /**
302
+ * Check the binary's version, write the composed root, init it and plan
303
+ * once, then cut the plan into each member's part. The composed root is
304
+ * taken away again before this returns.
305
+ */
306
+ async plan(): Promise<ComposedRunPlan> {
307
+ const fallback = await this.fallback();
308
+ if (fallback) {
309
+ this.log(`composed: ${fallback.reason}; the estates plan wave by wave`);
310
+ return this.notPlanned({ fallback });
311
+ }
312
+ let root: ComposedRoot;
313
+ try {
314
+ root = this.writeRoot();
315
+ } catch (e) {
316
+ return this.notPlanned({ error: `the composed root could not be written: ${(e as Error).message}` });
317
+ }
318
+ const planFile = join(this.work, "composed.tfplan");
319
+ let plan: unknown;
320
+ let text = "";
321
+ let summary = "";
322
+ try {
323
+ const env = this.env();
324
+ const init = await this.exec(this.binary, ["init", "-input=false", "-no-color"], this.composeDir, env);
325
+ if (init.code !== 0) return this.notPlanned({ error: `the composed root's init failed\n${firstLines(init.out)}` });
326
+ const p = await this.exec(this.binary, ["plan", "-input=false", "-no-color", `-out=${planFile}`], this.composeDir, env);
327
+ if (p.code !== 0) return this.notPlanned({ error: `the composed plan failed\n${firstLines(p.out)}` });
328
+ summary = p.stdout.match(/Plan: .*|No changes\..*/)?.[0] ?? "planned";
329
+ const json = await this.exec(this.binary, ["show", "-json", planFile], this.composeDir, env);
330
+ try {
331
+ plan = JSON.parse(json.stdout);
332
+ } catch {
333
+ return this.notPlanned({ error: `show -json printed no plan\n${firstLines(json.out, 6)}` });
334
+ }
335
+ text = (await this.exec(this.binary, ["show", "-no-color", planFile], this.composeDir, env)).stdout;
336
+ } finally {
337
+ this.restoreRoot();
338
+ }
339
+ const members = composedMemberPlans(this.c, plan, { planner: "choudoufu", scope: (m) => this.estateOf(m) });
340
+ const texts = Object.fromEntries(this.c.order.map((m) => [m, sliceText(text, this.c.modules.get(m)!, members[m]!.counts)] as const));
341
+ const digest = computePlanDigest("terraform-compose-estate", { plan: terraformPlanDigest(plan), modules: this.c.order.map((m) => [m, this.c.modules.get(m)]), generated: root.generated });
342
+ this.planned = { members, digest, plan, planFile, text, texts, generated: root.generated, counts: planCounts(plan), summary };
343
+ return this.planned;
344
+ }
345
+
346
+ /**
347
+ * Apply the saved plan from the composed root, then plan again and read
348
+ * each member's values from that plan's `prior_state`. A failed apply
349
+ * leaves what applied applied, and each member's outcome says how far its
350
+ * part got.
351
+ */
352
+ async apply(): Promise<ComposedRunApply> {
353
+ const p = this.planned;
354
+ const notStarted = (error: string): ComposedRunApply => ({
355
+ ok: false,
356
+ members: Object.fromEntries(this.c.order.map((m) => [m, { status: "not-started" as const, planned: p?.members[m]?.counts.changes ?? 0, done: 0 }] as const)),
357
+ error,
358
+ });
359
+ if (!p?.plan) return notStarted("the composed run did not plan");
360
+ let root: ComposedRoot;
361
+ try {
362
+ root = this.writeRoot();
363
+ } catch (e) {
364
+ return notStarted(`the composed root could not be written: ${(e as Error).message}`);
365
+ }
366
+ let out = "";
367
+ let code = 1;
368
+ let after: unknown;
369
+ try {
370
+ if (JSON.stringify(root.generated) !== JSON.stringify(p.generated)) return notStarted("the composed root differs from the one planned, so nothing applied");
371
+ const a = await this.exec(this.binary, ["apply", "-input=false", "-no-color", p.planFile], this.composeDir, this.env());
372
+ out = a.out;
373
+ code = a.code;
374
+ for (const line of a.stdout.split("\n")) if (line.trim()) this.log(`composed: ${line}`);
375
+ after = await this.planAfter();
376
+ } finally {
377
+ this.restoreRoot();
378
+ }
379
+ const planned = new Map(this.c.order.map((m) => [m, p.members[m]!.counts.changes] as const));
380
+ const slices = after !== undefined ? new Map(this.c.order.map((m) => [m, slicePlan(after, this.c.modules.get(m)!)] as const)) : undefined;
381
+ const remaining = slices ? new Map(this.c.order.map((m) => [m, planCounts(slices.get(m)).changes] as const)) : undefined;
382
+ const ended = estateOutcomes(this.c, planned, out, code, remaining);
383
+ const members: Record<string, ComposedMemberOutcome> = {};
384
+ for (const m of this.c.order) {
385
+ const row: ComposedMemberOutcome = { ...ended.members[m]! };
386
+ const slice = slices?.get(m);
387
+ if (slice !== undefined) {
388
+ row.resources = stateResources(slice);
389
+ if (row.status === "applied" || row.status === "no-changes") {
390
+ // A state choudoufu builds from records may carry no root outputs; the same plan's planned outputs, all known after the apply, stand in.
391
+ const outputs = stateOutputs(slice);
392
+ row.outputs = Object.keys(outputs).length > 0 ? outputs : terraformPlannedOutputs(slice).known;
393
+ }
394
+ }
395
+ members[m] = row;
396
+ }
397
+ const ok = code === 0 && Object.values(members).every((x) => x.status === "applied" || x.status === "no-changes");
398
+ const error = ok ? undefined : (ended.other[0] ?? Object.values(members).find((x) => x.error)?.error ?? "the composed apply failed");
399
+ return { ok, members, ...(after !== undefined ? { after } : {}), ...(error ? { error } : {}) };
400
+ }
401
+
402
+ /**
403
+ * `show -json` of a plan of the composed root made after the apply, whose
404
+ * `prior_state` choudoufu builds from the records and the live system.
405
+ * Undefined when it cannot be made: the values unknown at plan then stay
406
+ * unknown. Never fails the run.
407
+ */
408
+ private async planAfter(): Promise<unknown> {
409
+ const planFile = join(this.work, "composed-after.tfplan");
410
+ const env = this.env();
411
+ try {
412
+ const p = await this.exec(this.binary, ["plan", "-input=false", "-no-color", `-out=${planFile}`], this.composeDir, env);
413
+ if (p.code !== 0) {
414
+ this.log(`composed: the estates could not be planned after the apply; the values unknown at plan stay unknown\n${firstLines(p.out)}`);
415
+ return undefined;
416
+ }
417
+ const s = await this.exec(this.binary, ["show", "-json", planFile], this.composeDir, env);
418
+ if (s.code !== 0) return undefined;
419
+ return JSON.parse(s.stdout) as unknown;
420
+ } catch (e) {
421
+ this.log(`composed: the estates could not be read after the apply (${(e as Error).message}); the values unknown at plan stay unknown`);
422
+ return undefined;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Take the composed root away if a call left it, with the binary's working
428
+ * files under `.chant-compose/`, and the plan files. There is no lock to
429
+ * let go of. Safe to call more than once.
430
+ */
431
+ async release(): Promise<void> {
432
+ this.restoreRoot();
433
+ for (const f of [".terraform", ".terraform.lock.hcl"]) rmSync(join(this.composeDir, f), { recursive: true, force: true });
434
+ try {
435
+ if (existsSync(this.composeDir) && readdirSync(this.composeDir).length === 0) rmdirSync(this.composeDir);
436
+ } catch {
437
+ // Gone already.
438
+ }
439
+ if (this.madeWork) rmSync(this.work, { recursive: true, force: true });
440
+ else for (const f of ["composed.tfplan", "composed-after.tfplan"]) rmSync(join(this.work, f), { force: true });
441
+ this.planned = undefined;
442
+ }
443
+ }