@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
package/src/hcl/parse.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * The one shared HCL parse for the terraform lexicon.
3
3
  *
4
- * Two entry points, one entity shape. `parseTerraformRootDir` reads the `.tf`
5
- * files of a directory (non-recursive, matching Terraform's own module
6
- * scoping) and is what `buildRoots()` calls. `parseTerraformRootContent` takes
7
- * the joined-file string `chant audit` hands a lexicon for a discovered root
8
- * module: every `.tf` in filename order behind a `# file: <name>` line comment.
4
+ * Two entry points, one entity shape. `parseTerraformRootDir` reads the
5
+ * configuration files of a directory (non-recursive, matching Terraform's own
6
+ * module scoping; `.tf` and `.tf.json`, plus `.tofu` and `.tofu.json` for a
7
+ * tofu or choudoufu root, see `./files.ts`) and is what `buildRoots()` calls.
8
+ * `parseTerraformRootContent` takes the joined-file string `chant audit` hands
9
+ * a lexicon for a discovered root module: every configuration file in filename
10
+ * order behind a `# file: <name>` line comment, narrowed to the same set.
9
11
  * Both funnel through `blocksToEntities`, so an entity the audit path sees is
10
12
  * the entity the build path sees.
11
13
  *
@@ -18,7 +20,7 @@
18
20
  * changed in `packages/core/package.json` for this.
19
21
  */
20
22
 
21
- import { existsSync, readdirSync, readFileSync } from "node:fs";
23
+ import { existsSync, readFileSync } from "node:fs";
22
24
  import { join } from "node:path";
23
25
  import { loadHcl2json, type Hcl2Json } from "@intentius/chant/terraform/hcl2json";
24
26
  import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
@@ -27,6 +29,20 @@ import type { SuppressionDirective } from "@intentius/chant/lint/suppressions";
27
29
  import type { TerraformDeleteMode } from "../config";
28
30
  import { redactSensitiveBody, redactSensitiveSource } from "./redact";
29
31
  import { scanSuppressions, directivesFor, type FileScan } from "./suppressions";
32
+ import { isJsonConfigFile, jsonConfigTree, listConfigFiles, selectConfigFiles } from "./files";
33
+
34
+ export {
35
+ TERRAFORM_SUFFIXES,
36
+ TOFU_SUFFIXES,
37
+ isConfigFileFor,
38
+ isJsonConfigFile,
39
+ isTofuConfigFile,
40
+ jsonConfigTree,
41
+ listConfigFiles,
42
+ listConfigFilesSafe,
43
+ readsTofuFiles,
44
+ selectConfigFiles,
45
+ } from "./files";
30
46
 
31
47
  /** A parsed HCL block body, as `@cdktn/hcl2json` encodes it. */
32
48
  export type BlockBody = Record<string, unknown>;
@@ -40,6 +56,10 @@ export type BlockBody = Record<string, unknown>;
40
56
  * the instance count is a plan-time answer and often a state-time one, but it
41
57
  * is a shape a reader should be told rather than left to infer from a node
42
58
  * count that never grows.
59
+ *
60
+ * Since #4454 a literal expansion also gets its instance nodes, children of
61
+ * the block's node (`TerraformEntity.instanceKeys`). The block node stays,
62
+ * and the edges stay block to block.
43
63
  */
44
64
  export type TerraformExpansion = "count" | "for_each";
45
65
 
@@ -151,6 +171,27 @@ export interface TerraformEntity extends Declarable {
151
171
  * ROOT's name, not the module's: the module is not separately applied.
152
172
  */
153
173
  readonly stack: string;
174
+ /**
175
+ * The instance keys a `resource` or `data` block's literal `count` or
176
+ * `for_each` expands to (#4454), as Terraform prints them: `[0]`, `[1]`
177
+ * for `count = 2`, `["a"]` for a `for_each` over a literal map or
178
+ * `toset([...])`. Core's `withInstanceNodes` reads this lexicon-neutral
179
+ * field, as `buildGraphIr` reads `stack`, and draws one instance node per
180
+ * key under the block's node. Absent when the block is not expanded or its
181
+ * expansion is not a literal. Since #4486 `./instances.ts` also sets it
182
+ * for an expansion it can count from the root's variables (`count = var.n`
183
+ * with `n` known), and for a `module` call.
184
+ */
185
+ readonly instanceKeys?: readonly string[];
186
+ /**
187
+ * The instance nodes of a block under an expanded module call (#4486), by
188
+ * full node id: `estate/module.app[0]/aws_eip.pool[1]`, with `key` the
189
+ * block's own key (`[1]`, absent when the block is not expanded itself)
190
+ * and `scope` the module path as Terraform prints it (`module.app[0]`).
191
+ * Core's `entityInstances` reads this in place of `instanceKeys`, which
192
+ * such a block never carries. Set by `./instances.ts`.
193
+ */
194
+ readonly instances?: ReadonlyArray<{ readonly id: string; readonly key?: string; readonly scope?: string }>;
154
195
  /**
155
196
  * Every reference in this block, resolved to the entity keys it points at
156
197
  * (chant #2265), in core's lexicon-neutral {@link EntityReference} shape.
@@ -183,6 +224,23 @@ export const OUTPUT_TYPE = "Terraform::Output";
183
224
  export const LOCALS_TYPE = "Terraform::Locals";
184
225
  /** A `live { estate = "..." }` block, or an `estate.chdf.hcl` sidecar's content (#2103). */
185
226
  export const LIVE_TYPE = "Terraform::Live";
227
+ /**
228
+ * The four configuration blocks that change how a plan treats addresses
229
+ * rather than declaring anything (#4458): `moved`, `import`, `removed` and
230
+ * `check`. The first three carry no label and key as `moved`, `import`,
231
+ * `removed` (numbered `~2`, `~3` when a root holds several, as `locals` is);
232
+ * a `check` keys as `check.<name>`. Their references are edges like any other
233
+ * block's: a `moved` block's `from` and `to`, an `import` block's `to`, a
234
+ * `removed` block's `from`, and everything a `check` block's assertions and
235
+ * scoped `data` blocks read. A `from` usually names an address that is no
236
+ * longer declared, which resolves to no entity and so draws no edge, and
237
+ * that is all that happens: an unresolved reference is dropped, never
238
+ * reported (`./edges.ts`).
239
+ */
240
+ export const MOVED_TYPE = "Terraform::Moved";
241
+ export const IMPORT_TYPE = "Terraform::Import";
242
+ export const REMOVED_TYPE = "Terraform::Removed";
243
+ export const CHECK_TYPE = "Terraform::Check";
186
244
 
187
245
  /**
188
246
  * Terragrunt config entities (#3417). A `terragrunt.hcl` or `root.hcl` is not
@@ -214,6 +272,55 @@ function expansionOf(body: BlockBody): TerraformExpansion | undefined {
214
272
  return undefined;
215
273
  }
216
274
 
275
+ /** The most instance keys read off one literal; past it the block draws as the block alone. */
276
+ const MAX_LITERAL_INSTANCES = 256;
277
+
278
+ /**
279
+ * The instance keys a literal expansion gives (#4454), as Terraform prints
280
+ * them, or undefined when the expansion is not a literal a read of the
281
+ * source can count. `count` as a number (`count = 3`), `for_each` as a
282
+ * literal map (`{ a = 1 }`, keys in Terraform's lexical order) or
283
+ * `toset([...])` over string literals. Anything holding a reference is a
284
+ * plan-time answer.
285
+ */
286
+ export function literalInstanceKeys(body: BlockBody): string[] | undefined {
287
+ const keys = literalKeys(body);
288
+ if (keys === undefined || keys.length > MAX_LITERAL_INSTANCES) return undefined;
289
+ return keys;
290
+ }
291
+
292
+ function literalKeys(body: BlockBody): string[] | undefined {
293
+ if ("count" in body) {
294
+ const count = body.count;
295
+ const n = typeof count === "number" ? count
296
+ : typeof count === "string" && /^\$\{\s*\d+\s*\}$/.test(count.trim()) ? Number(count.trim().slice(2, -1))
297
+ : undefined;
298
+ if (n === undefined || !Number.isInteger(n) || n < 0) return undefined;
299
+ return Array.from({ length: n }, (_, i) => `[${i}]`);
300
+ }
301
+ if ("for_each" in body) {
302
+ const each = body.for_each;
303
+ if (typeof each === "object" && each !== null && !Array.isArray(each)) {
304
+ const names = Object.keys(each);
305
+ if (names.some((k) => k.includes("${"))) return undefined;
306
+ return names.sort().map((k) => `[${JSON.stringify(k)}]`);
307
+ }
308
+ if (typeof each === "string") {
309
+ const m = /^\$\{\s*toset\(\s*(\[[\s\S]*\])\s*\)\s*\}$/.exec(each.trim());
310
+ if (!m) return undefined;
311
+ let list: unknown;
312
+ try {
313
+ list = JSON.parse(m[1]!.replace(/,\s*\]$/, "]"));
314
+ } catch {
315
+ return undefined;
316
+ }
317
+ if (!Array.isArray(list) || !list.every((v) => typeof v === "string")) return undefined;
318
+ return [...new Set(list as string[])].sort().map((k) => `[${JSON.stringify(k)}]`);
319
+ }
320
+ }
321
+ return undefined;
322
+ }
323
+
217
324
  /**
218
325
  * Build one entity. Written out rather than run through `createResource`
219
326
  * from `@intentius/chant/runtime`: that factory is for generated resource
@@ -234,6 +341,7 @@ export function terraformEntity(
234
341
  suppressions?: readonly SuppressionDirective[],
235
342
  ): TerraformEntity {
236
343
  const expansion = expansionOf(body);
344
+ const instanceKeys = expansion && (entityType === RESOURCE_TYPE || entityType === DATA_TYPE) ? literalInstanceKeys(body) : undefined;
237
345
  const labels = entityType === RESOURCE_TYPE ? blockLabels(address)
238
346
  : entityType === DATA_TYPE && address.startsWith("data.") ? blockLabels(address.slice("data.".length))
239
347
  : undefined;
@@ -243,6 +351,7 @@ export function terraformEntity(
243
351
  entityType,
244
352
  kind: "resource",
245
353
  stack: root,
354
+ ...(instanceKeys ? { instanceKeys } : {}),
246
355
  props: {
247
356
  address,
248
357
  ...(labels ?? {}),
@@ -302,11 +411,20 @@ export function splitBundleContent(content: string, fallbackName = "main.tf"): T
302
411
  return files.filter((f) => f.source.trim() !== "");
303
412
  }
304
413
 
305
- /** Every `.tf` directly under `dir`, in filename order. Non-recursive. */
306
- export function listTerraformFiles(dir: string): string[] {
307
- return readdirSync(dir)
308
- .filter((f) => f.endsWith(".tf"))
309
- .sort();
414
+ /**
415
+ * Every configuration file directly under `dir` that a root run by `binary`
416
+ * reads, in filename order, non-recursive: `.tf` and `.tf.json`, plus `.tofu`
417
+ * and `.tofu.json` for a tofu or choudoufu root, with `x.tofu` shadowing
418
+ * `x.tf` (chant#4457). The shared lister is `./files.ts`'s `listConfigFiles`.
419
+ */
420
+ export function listTerraformFiles(dir: string, binary?: string): string[] {
421
+ return listConfigFiles(dir, binary);
422
+ }
423
+
424
+ /** One file's tree in hcl2json's shape: hcl2json for HCL, {@link jsonConfigTree} for Terraform's JSON syntax. */
425
+ export async function parseFileTree(parser: Hcl2Json, file: TerraformFile): Promise<Record<string, unknown>> {
426
+ if (isJsonConfigFile(file.name)) return jsonConfigTree(file.source);
427
+ return (await parser.parse(file.name, file.source)) as Record<string, unknown>;
310
428
  }
311
429
 
312
430
  /** The Terragrunt config files directly under `dir` (`root.hcl`, `terragrunt.hcl`), in filename order like the audit's bundle. */
@@ -488,7 +606,7 @@ export async function blocksToEntities(
488
606
  };
489
607
 
490
608
  for (const file of files) {
491
- const tree = (await parser.parse(file.name, file.source)) as Record<string, unknown>;
609
+ const tree = await parseFileTree(parser, file);
492
610
  const scan = scanSuppressions(file.name, file.source);
493
611
  if (isTerragruntFileName(file.name)) {
494
612
  terragruntBlocks(tree, file, scan, add);
@@ -503,6 +621,10 @@ export async function blocksToEntities(
503
621
  oneLabel(tree, "output", OUTPUT_TYPE, (n) => `output.${n}`, file, scan);
504
622
  twoLabels(tree, "resource", RESOURCE_TYPE, (t, n) => `${t}.${n}`, file, scan);
505
623
  twoLabels(tree, "data", DATA_TYPE, (t, n) => `data.${t}.${n}`, file, scan);
624
+ unlabelled(tree, "moved", MOVED_TYPE, file, scan);
625
+ unlabelled(tree, "import", IMPORT_TYPE, file, scan);
626
+ unlabelled(tree, "removed", REMOVED_TYPE, file, scan);
627
+ oneLabel(tree, "check", CHECK_TYPE, (n) => `check.${n}`, file, scan);
506
628
  }
507
629
 
508
630
  if (sidecar) {
@@ -568,8 +690,9 @@ function terragruntBlocks(
568
690
  }
569
691
 
570
692
  /**
571
- * Parse a root module directory. Reads every `.tf` directly under `dir`, plus
572
- * the `estate.chdf.hcl` sidecar beside them when present. Throws whatever the
693
+ * Parse a root module directory. Reads the configuration files directly under
694
+ * `dir` that `modeOptions.binary` reads ({@link listTerraformFiles}), plus the
695
+ * `estate.chdf.hcl` sidecar beside them when present. Throws whatever the
573
696
  * parser throws for malformed HCL; `buildRoots()` is where that becomes a
574
697
  * warning.
575
698
  */
@@ -581,7 +704,7 @@ export async function parseTerraformRootDir(
581
704
  callers: readonly string[] = [],
582
705
  ): Promise<Map<string, Declarable>> {
583
706
  // A child module is a Terraform module, never a Terragrunt unit (#3417).
584
- const names = [...listTerraformFiles(dir), ...(callers.length === 0 ? listTerragruntFiles(dir) : [])];
707
+ const names = [...listTerraformFiles(dir, modeOptions?.binary), ...(callers.length === 0 ? listTerragruntFiles(dir) : [])];
585
708
  const files = names.map((name) => ({
586
709
  name,
587
710
  source: readFileSync(join(dir, name), "utf-8"),
@@ -610,7 +733,17 @@ export async function parseTerraformRootContent(
610
733
  hcl2json?: Hcl2Json,
611
734
  modeOptions?: TerraformRootModeOptions,
612
735
  ): Promise<Map<string, Declarable>> {
613
- return blocksToEntities(splitBundleContent(content), root, hcl2json, modeOptions);
736
+ return blocksToEntities(bundleFilesFor(splitBundleContent(content), modeOptions?.binary), root, hcl2json, modeOptions);
737
+ }
738
+
739
+ /**
740
+ * The files of a joined bundle a root run by `binary` reads: the same set
741
+ * {@link listTerraformFiles} gives for a directory holding them, plus the
742
+ * Terragrunt files, in bundle order (chant#4457).
743
+ */
744
+ export function bundleFilesFor(files: readonly TerraformFile[], binary?: string): TerraformFile[] {
745
+ const keep = new Set(selectConfigFiles(files.map((f) => f.name), binary));
746
+ return files.filter((f) => keep.has(f.name) || isTerragruntFileName(f.name));
614
747
  }
615
748
 
616
749
  /** The root name an audit of the audited directory itself parses under. */
package/src/hcl/roots.ts CHANGED
@@ -34,6 +34,7 @@ import type { TerraformRootConfig } from "../config";
34
34
  import { LIVE_TYPE, parseTerraformRootDir } from "./parse";
35
35
  import { descendModules, resolveCallModuleType, type CallModuleType } from "./descend";
36
36
  import { resolveEntityReferences } from "./edges";
37
+ import { resolveInstanceNodes } from "./instances";
37
38
 
38
39
  export interface TerraformRootsResult {
39
40
  entities: Map<string, Declarable>;
@@ -111,6 +112,11 @@ export async function renderTerraformRoots(
111
112
  for (const [key, entity] of descended.entities) entities.set(key, entity);
112
113
  warnings.push(...descended.warnings);
113
114
 
115
+ // Instance nodes the root's variables let the declared graph count,
116
+ // and those under expanded module calls (#4486). Needs the root and
117
+ // every child scope parsed, so it runs once the descent has.
118
+ await resolveInstanceNodes(entities, name, { dir, ...(root.varFiles ? { varFiles: root.varFiles } : {}), hcl2json: opts.hcl2json });
119
+
114
120
  // A `live` block or `estate.chdf.hcl` sidecar only takes effect under
115
121
  // choudoufu; under any other binary it parses fine but is inert, which
116
122
  // is worth a warning rather than silence (#2103).
@@ -27,9 +27,9 @@
27
27
  import type { SuppressionDirective, SuppressionIds } from "@intentius/chant/lint/suppressions";
28
28
 
29
29
  /** A block header keyword with zero labels (`terraform {`, `locals {`). */
30
- const BARE_RE = /^\s*(terraform|locals|remote_state)\s*\{/;
30
+ const BARE_RE = /^\s*(terraform|locals|remote_state|moved|import|removed)\s*\{/;
31
31
  /** One label (`provider "aws" {`, `module "vpc" {`, `variable "x" {`, `output "x" {`). */
32
- const ONE_LABEL_RE = /^\s*(provider|module|variable|output|dependency|generate)\s+"([^"]*)"\s*\{/;
32
+ const ONE_LABEL_RE = /^\s*(provider|module|variable|output|check|dependency|generate)\s+"([^"]*)"\s*\{/;
33
33
  /** Two labels (`resource "aws_s3_bucket" "assets" {`, `data "aws_ami" "x" {`). */
34
34
  const TWO_LABEL_RE = /^\s*(resource|data)\s+"([^"]*)"\s+"([^"]*)"\s*\{/;
35
35
 
@@ -45,6 +45,12 @@ function addressFor(keyword: string, labels: string[]): string {
45
45
  return "locals";
46
46
  case "remote_state":
47
47
  return "remote_state";
48
+ case "moved":
49
+ case "import":
50
+ case "removed":
51
+ return keyword;
52
+ case "check":
53
+ return `check.${labels[0]}`;
48
54
  case "dependency":
49
55
  return `dependency.${labels[0]}`;
50
56
  case "generate":
package/src/index.ts CHANGED
@@ -10,10 +10,85 @@ export {
10
10
  terraformApplyCapability,
11
11
  TerraformPlanMovedError,
12
12
  TERRAFORM_VERB_FAMILIES,
13
+ // #4455: a choudoufu member's outputs, read for core's `member-outputs`.
14
+ choudoufuOutputSource,
15
+ choudoufuMemberPlannedOutputs,
13
16
  type TerraformApplyStepInput,
14
17
  type TerraformApplyStepOutput,
15
18
  } from "./components/capability-plugin";
16
19
 
20
+ // Composed runs (#4442): the working state merged and split, the composed
21
+ // plan cut into one plan per member, and each member's apply outcome.
22
+ export { mergeStates, splitState, splitStates, derivedLineage, COMPOSE_LINEAGE, type SplitState } from "./compose/state-merge";
23
+ export {
24
+ slicePlan as sliceComposedPlan,
25
+ sliceText,
26
+ planCounts,
27
+ composedReads,
28
+ composedMemberPlans,
29
+ type PlanCounts,
30
+ type ComposedMemberRead,
31
+ type ComposedMemberPlan,
32
+ type ComposedMemberPlansOptions,
33
+ } from "./compose/slice";
34
+ export { readApplyEvents, memberApply, composedApplyOutcomes, type ApplyEvents, type MemberApply, type MemberApplyOutcome } from "./compose/apply-events";
35
+ export { topModule, stripModule, stripChange, moduleChanges } from "./compose/module-address";
36
+
37
+ // Composed runs of choudoufu estates (#4445): one plan and one apply, values read from a post-apply plan.
38
+ export {
39
+ EstateRun,
40
+ estateVersionFallback,
41
+ estateOutcomes,
42
+ readEstateApplyText,
43
+ type ComposedRunner,
44
+ type ComposedRunPlan,
45
+ type ComposedRunApply,
46
+ type ComposedMemberOutcome,
47
+ type EstateRunOptions,
48
+ } from "./compose/estate";
49
+ export { priorStateValues, stateResources, stateOutputs, type StateResourceValues } from "./applied-state";
50
+
51
+ // Composed runs over roots with state (#4444): one plan and one apply, each
52
+ // state written back whatever happened. Implements `ComposedRunner` (#4445).
53
+ export {
54
+ ComposedRun,
55
+ composedPlanDigest,
56
+ changesSomething,
57
+ type ComposedRunOptions,
58
+ type ComposedRunStage,
59
+ type StateRunPlan,
60
+ type StateRunApply,
61
+ type StateMemberOutcome,
62
+ type ComposedStateWrite,
63
+ type ComposeHook,
64
+ type ComposeHookMoment,
65
+ } from "./compose/run";
66
+
67
+ // The plan shape of a `show -json` plan (#4436), for core's `coveredRound`.
68
+ export { terraformPlanShape, type TerraformPlanShapeOptions } from "./plan-shape";
69
+
70
+ // Composed runs (#4419, #4438): which roots compose, in what order, and why
71
+ // the others fall back. Off unless `terraform.compose.enabled` is set.
72
+ export {
73
+ compositionFor,
74
+ compositionIfEnabled,
75
+ composeOff,
76
+ composeSet,
77
+ readOrder,
78
+ moduleNames,
79
+ fallbackLine,
80
+ withPlanFailed,
81
+ identityKey,
82
+ DEFAULT_COMPOSE_MAX,
83
+ COMPOSE_STEP_KIND,
84
+ type Composition,
85
+ type CompositionInput,
86
+ type ComposedRead,
87
+ type ComposeFallback,
88
+ type ComposeFallbackKind,
89
+ type ExpectedBinary,
90
+ } from "./compose";
91
+
17
92
  // Serializer
18
93
  export { terraformSerializer } from "./serializer";
19
94
 
@@ -68,6 +143,7 @@ export {
68
143
  choudoufuLiveLs,
69
144
  choudoufuLiveCheck,
70
145
  choudoufuAdopt,
146
+ choudoufuPlanSet,
71
147
  terraformPinRollout,
72
148
  } from "./op/builders";
73
149
 
@@ -0,0 +1,54 @@
1
+ /**
2
+ * choudoufu's live-plan omission codes, in chant's vocabulary.
3
+ *
4
+ * Its own module so the observation reader (`./describe-resources.ts`) and
5
+ * the drift adapters (`./drift-record.ts`) read one table, and the drift
6
+ * adapters bundle without the reader's process and file-system imports.
7
+ */
8
+
9
+ import type { UnobservedReason } from "@intentius/chant/observation";
10
+
11
+ /**
12
+ * choudoufu's omission reason codes, mapped onto chant's total vocabulary
13
+ * (`internal/live/projection/result.go`). `"absent"` is not an
14
+ * {@link UnobservedReason}: it is the tri-state's OBSERVED-ABSENT, the one
15
+ * omission that is a claim of knowledge rather than of ignorance.
16
+ *
17
+ * A code this table does not know maps to `read-failed`, which is the safe
18
+ * direction: no NOT-OBSERVED verdict ever becomes a create or a delete.
19
+ */
20
+ export const LIVE_PLAN_OMISSION_REASONS: Readonly<Record<string, UnobservedReason | "absent">> = {
21
+ // The provider was asked and reported, normally, that nothing is there.
22
+ ABSENT: "absent",
23
+ // The provider errored, or could not be reached in a usable order.
24
+ FAILED: "read-failed",
25
+ PARENT_UNAVAILABLE: "read-failed",
26
+ CYCLE: "read-failed",
27
+ INCOMPLETE_BLOCK: "read-failed",
28
+ LISTED_NOT_IMPORTABLE: "read-failed",
29
+ // Something exists at the identity and does not carry this estate's marker.
30
+ // Normally answered by the `unowned[]` section before this table is read;
31
+ // this entry is what happens if a document ever carries the omission alone.
32
+ UNOWNED: "read-failed",
33
+ // The mode cannot name, list or tag this instance at all: a server-assigned
34
+ // identity with no marker to find it by, a resource with no cloud object to
35
+ // read, or an address another declared instance has taken over.
36
+ NEEDS_DISCOVERY: "unsupported-kind",
37
+ UNREADABLE: "unsupported-kind",
38
+ SUPERSEDED: "unsupported-kind",
39
+ // The run's own tag index saw an object at the identity and the import
40
+ // read could not confirm it (choudoufu #1043). The read failed; the
41
+ // object is not proposed for creation.
42
+ VERIFIED_NOT_IMPORTABLE: "read-failed",
43
+ // -target or -exclude left the block out of the plan graph (choudoufu
44
+ // #1176): nothing was read because the caller asked for it not to be.
45
+ OUT_OF_SCOPE: "filtered",
46
+ };
47
+
48
+ /** Does this text name a credentials or authorization failure rather than any other error? */
49
+ export function readsAsCredentials(text: string): boolean {
50
+ return /\b(no valid credential|credentials?|not authorized|unauthorized|accessdenied|access denied|expiredtoken|invalidclienttokenid|signature)\b/i.test(
51
+ text,
52
+ );
53
+ }
54
+
@@ -104,7 +104,7 @@ describe("LSP completions", () => {
104
104
  it("completes root-entry keys inside chant.config.ts's terraform.roots.<name>", () => {
105
105
  const content = "export default {\n terraform: {\n roots: {\n app: {\n ";
106
106
  const items = completions(ctx({ content, position: { line: 4, character: 8 } }));
107
- expect(items.map((i) => i.label).sort()).toEqual(["backendConfig", "delete", "dir", "varFiles", "workspace"]);
107
+ expect(items.map((i) => i.label).sort()).toEqual(["backendConfig", "backendConfigFiles", "delete", "dir", "signalOnly", "varFiles", "workspace"]);
108
108
  });
109
109
 
110
110
  it("completes the terraform namespace's own top-level keys", () => {
@@ -25,7 +25,9 @@ export const ROOT_ENTRY_KEYS: OptionKey[] = [
25
25
  { key: "workspace", detail: "Terraform workspace to select for this root. Omitted means `default`." },
26
26
  { key: "varFiles", detail: "`-var-file` arguments, in order, relative to `dir`." },
27
27
  { key: "backendConfig", detail: "`-backend-config` key/value pairs handed to `init`." },
28
+ { key: "backendConfigFiles", detail: "Backend-config files handed to `init` as `-backend-config=<path>`, before the pairs, relative to `dir`." },
28
29
  { key: "delete", detail: '"never" | "owned-only" | "gated" — this root\'s delete mode, mapped onto choudoufu\'s policy block on a live root.' },
30
+ { key: "signalOnly", detail: "`true` when the root is declared only so a change to it brings this project into the change. The build and live reads skip it." },
29
31
  ];
30
32
 
31
33
  /** `TerraformApplyOpConfig`'s keys (`TerraformApplyOp({ <here> })`). */
@@ -0,0 +1,111 @@
1
+ /**
2
+ * `choudoufuPlanSet` as an Op activity (#4483, H3 of #4477). One Op plans two
3
+ * live choudoufu roots, one per cloud, and hands each root's `changeSet` to
4
+ * `composeChangeSet`, the shape behold's three-cloud front door takes instead
5
+ * of composing the change set in a script.
6
+ *
7
+ * The activity is the real one, resolved through `loadActivities`, so it runs
8
+ * the configured binary. A fake `choudoufu` on `PATH` answers `version` and
9
+ * prints a one-root set plan document for `live-plan-set`, exiting 2 as
10
+ * choudoufu does for a plan with changes. Nothing reaches a cloud.
11
+ */
12
+
13
+ import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { afterAll, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
17
+ import { composeChangeSet, loadActivities, runOpLocally, validateActivitySteps, validateStepOutputRefs, type ActivityProfile, type OpConfig } from "@intentius/chant/op";
18
+ import { choudoufuPlanSet } from "../builders";
19
+ import * as contracts from "../activity-contracts";
20
+ import { __resetChoudoufuVersionCheckForTests } from "./terraform";
21
+
22
+ const scratch: string[] = [];
23
+ afterAll(() => {
24
+ for (const d of scratch) rmSync(d, { recursive: true, force: true });
25
+ });
26
+ beforeEach(() => __resetChoudoufuVersionCheckForTests());
27
+ afterEach(() => vi.unstubAllEnvs());
28
+
29
+ const PROFILES: Record<string, ActivityProfile> = {
30
+ longInfra: { timeout: "5m", retry: { maximumAttempts: 1 } },
31
+ fastIdempotent: { timeout: "5m", retry: { maximumAttempts: 1 } },
32
+ };
33
+
34
+ /** A fake choudoufu: `version`, and `live-plan-set -json -out-dir=<d> <root>` printing a one-root document. */
35
+ const FAKE = `#!/bin/sh
36
+ if [ "$1" = "version" ]; then echo "choudoufu v0.31.0 (based on OpenTofu v1.13.0)"; exit 0; fi
37
+ if [ "$1" = "live-plan-set" ]; then
38
+ for a in "$@"; do root="$a"; done
39
+ name=$(basename "$root")
40
+ cat <<EOF
41
+ {"format_version":"1","roots":[{"root":"$root","estate":"$name","status":"planned","error":"","changes":true,"plan":{"format_version":"1.2","resource_changes":[{"address":"null_resource.$name","mode":"managed","type":"null_resource","name":"$name","change":{"actions":["create"],"before":null,"after":{}}}]},"digest":"sha256:root-$name"}],"summary":{"roots":1,"planned":1,"changed":1,"failed":0},"digest":"sha256:set-$name","exit_code":2}
42
+ EOF
43
+ exit 2
44
+ fi
45
+ echo "fake choudoufu: unexpected $*" >&2
46
+ exit 1
47
+ `;
48
+
49
+ function project(): string {
50
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "chant-plan-set-op-")));
51
+ scratch.push(dir);
52
+ const bin = join(dir, "bin");
53
+ mkdirSync(bin);
54
+ writeFileSync(join(bin, "choudoufu"), FAKE);
55
+ chmodSync(join(bin, "choudoufu"), 0o755);
56
+ vi.stubEnv("PATH", `${bin}:${process.env.PATH ?? ""}`);
57
+ for (const root of ["aws", "azure"]) {
58
+ mkdirSync(join(dir, "estates", root), { recursive: true });
59
+ writeFileSync(join(dir, "estates", root, "main.tf"), "");
60
+ }
61
+ writeFileSync(
62
+ join(dir, "chant.config.json"),
63
+ JSON.stringify({ lexicons: ["terraform"], terraform: { binary: "choudoufu", roots: { aws: { dir: "estates/aws" }, azure: { dir: "estates/azure" } } } }),
64
+ );
65
+ return dir;
66
+ }
67
+
68
+ function frontDoor(cwd: string): OpConfig {
69
+ const aws = choudoufuPlanSet("aws", { cwd, id: "aws" });
70
+ const azure = choudoufuPlanSet("azure", { cwd, id: "azure" });
71
+ return {
72
+ name: "front-door-plan",
73
+ overview: "",
74
+ phases: [
75
+ { name: "Plan", steps: [aws, azure] },
76
+ { name: "Compose", steps: [composeChangeSet({ parts: [aws.out.changeSet, azure.out.changeSet], id: "doc" })] },
77
+ ],
78
+ };
79
+ }
80
+
81
+ describe("choudoufuPlanSet as an Op activity (#4483)", () => {
82
+ test("is resolved from the terraform lexicon's activities", async () => {
83
+ const activities = await loadActivities(["terraform"]);
84
+ expect(typeof activities.get("choudoufuPlanSet")).toBe("function");
85
+ });
86
+
87
+ test("its contract validates the steps and the changeSet references", () => {
88
+ const map = new Map(Object.values(contracts).map((c) => [c.name, c]));
89
+ const config = frontDoor("/tmp/x");
90
+ expect(validateActivitySteps(config, map)).toEqual([]);
91
+ expect(validateStepOutputRefs(config, map)).toEqual([]);
92
+ });
93
+
94
+ test("an Op plans two clouds' roots and composes one change set from their parts", async () => {
95
+ const dir = project();
96
+ const activities = await loadActivities(["terraform"]);
97
+ const compose = activities.get("composeChangeSet")!;
98
+ const documents: Array<{ document: { members?: Array<{ member: string }> }; digest: string }> = [];
99
+ activities.set("composeChangeSet", async (args, signal) => {
100
+ const out = (await compose(args, signal)) as (typeof documents)[number];
101
+ documents.push(out);
102
+ return out;
103
+ });
104
+ const result = await runOpLocally(frontDoor(dir), activities, PROFILES);
105
+ expect(result.status).toBe("ok");
106
+ expect(result.records.filter((r) => r.fn === "choudoufuPlanSet").map((r) => r.status)).toEqual(["ok", "ok"]);
107
+ expect(documents).toHaveLength(1);
108
+ expect(documents[0]!.digest).toMatch(/^jcs1-sha256:[0-9a-f]{64}$/);
109
+ expect((documents[0]!.document.members ?? []).map((m) => m.member).sort()).toEqual(["aws", "azure"]);
110
+ });
111
+ });
@@ -16,6 +16,10 @@
16
16
  * - choudoufuLiveLs: `live-ls -estate=<estate> -json [-consistent]` (#2103).
17
17
  * - choudoufuLiveCheck: `live-check -json`, no cloud calls (#2103).
18
18
  * - choudoufuAdopt: the tag writes that claim an adoption ledger's matches (#2105).
19
+ * - choudoufuPlanSet: `live-plan-set -json` over one live root, as a
20
+ * one-root set (#3459). Its `changeSet` is the root's part, so an Op can
21
+ * plan several clouds' roots and hand the parts to `composeChangeSet`
22
+ * (#4483).
19
23
  * - terraformPinRollout: one run of a pin-bump rollout, opening at most one
20
24
  * wave's pull request (#3189). It loads the HCL parser on first call.
21
25
  *
@@ -34,6 +38,8 @@ export {
34
38
  choudoufuLiveLs,
35
39
  choudoufuLiveCheck,
36
40
  choudoufuAdopt,
41
+ choudoufuPlanSet,
42
+ choudoufuPlanSetCommand,
37
43
  terraformInitCommand,
38
44
  terraformPlanCommand,
39
45
  terraformApplyCommand,
@@ -52,6 +58,7 @@ export {
52
58
  DEFAULT_TERRAFORM_BINARY,
53
59
  DEFAULT_LIVE_PLAN_DOCUMENT_FILE,
54
60
  MIN_CHOUDOUFU_VERSION,
61
+ MIN_CHOUDOUFU_ESTATES_VERSION,
55
62
  classifyApprovalRefusal,
56
63
  CHOUDOUFU_LIVE_PLAN_OUT_REFUSAL,
57
64
  CHOUDOUFU_APPROVAL_EXIT_CODE,
@@ -78,6 +85,8 @@ export type {
78
85
  ChoudoufuLiveLsResult,
79
86
  ChoudoufuLiveCheckResult,
80
87
  ChoudoufuAdoptResult,
88
+ ChoudoufuPlanSet,
89
+ ChoudoufuPlanSetResult,
81
90
  AdoptionRefusal,
82
91
  PlanChangeCounts,
83
92
  LivePlanUnownedCounts,