@intentius/chant-lexicon-terraform 0.124.52 → 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 (336) 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.map +1 -1
  27. package/dist/commands.js +4 -3
  28. package/dist/components/capability-plugin.d.ts +65 -3
  29. package/dist/components/capability-plugin.d.ts.map +1 -1
  30. package/dist/components/capability-plugin.js +197 -13
  31. package/dist/components/compose-group.d.ts +145 -0
  32. package/dist/components/compose-group.d.ts.map +1 -0
  33. package/dist/components/compose-group.js +407 -0
  34. package/dist/components/placeholder-preview.d.ts +78 -0
  35. package/dist/components/placeholder-preview.d.ts.map +1 -0
  36. package/dist/components/placeholder-preview.js +254 -0
  37. package/dist/compose/apply-events.d.ts +46 -0
  38. package/dist/compose/apply-events.d.ts.map +1 -0
  39. package/dist/compose/apply-events.js +77 -0
  40. package/dist/compose/estate.d.ts +194 -0
  41. package/dist/compose/estate.d.ts.map +1 -0
  42. package/dist/compose/estate.js +363 -0
  43. package/dist/compose/hcl.d.ts +99 -0
  44. package/dist/compose/hcl.d.ts.map +1 -0
  45. package/dist/compose/hcl.js +302 -0
  46. package/dist/compose/module-address.d.ts +24 -0
  47. package/dist/compose/module-address.d.ts.map +1 -0
  48. package/dist/compose/module-address.js +43 -0
  49. package/dist/compose/root.d.ts +127 -0
  50. package/dist/compose/root.d.ts.map +1 -0
  51. package/dist/compose/root.js +586 -0
  52. package/dist/compose/run.d.ts +183 -0
  53. package/dist/compose/run.d.ts.map +1 -0
  54. package/dist/compose/run.js +447 -0
  55. package/dist/compose/slice.d.ts +102 -0
  56. package/dist/compose/slice.d.ts.map +1 -0
  57. package/dist/compose/slice.js +249 -0
  58. package/dist/compose/state-address.d.ts +28 -0
  59. package/dist/compose/state-address.d.ts.map +1 -0
  60. package/dist/compose/state-address.js +154 -0
  61. package/dist/compose/state-merge.d.ts +52 -0
  62. package/dist/compose/state-merge.d.ts.map +1 -0
  63. package/dist/compose/state-merge.js +135 -0
  64. package/dist/compose.d.ts +179 -0
  65. package/dist/compose.d.ts.map +1 -0
  66. package/dist/compose.js +482 -0
  67. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  68. package/dist/composites/terraform-apply-op.js +7 -3
  69. package/dist/config.d.ts +59 -13
  70. package/dist/config.d.ts.map +1 -1
  71. package/dist/config.js +43 -9
  72. package/dist/describe-resources.d.ts +74 -12
  73. package/dist/describe-resources.d.ts.map +1 -1
  74. package/dist/describe-resources.js +374 -66
  75. package/dist/drift-read.d.ts +33 -0
  76. package/dist/drift-read.d.ts.map +1 -0
  77. package/dist/drift-read.js +243 -0
  78. package/dist/drift-record.d.ts +147 -0
  79. package/dist/drift-record.d.ts.map +1 -0
  80. package/dist/drift-record.js +383 -0
  81. package/dist/hcl/edges.d.ts.map +1 -1
  82. package/dist/hcl/edges.js +24 -1
  83. package/dist/hcl/files.d.ts +61 -0
  84. package/dist/hcl/files.d.ts.map +1 -0
  85. package/dist/hcl/files.js +221 -0
  86. package/dist/hcl/instances.d.ts +82 -0
  87. package/dist/hcl/instances.d.ts.map +1 -0
  88. package/dist/hcl/instances.js +390 -0
  89. package/dist/hcl/parse.d.ts +81 -9
  90. package/dist/hcl/parse.d.ts.map +1 -1
  91. package/dist/hcl/parse.js +114 -16
  92. package/dist/hcl/roots.d.ts.map +1 -1
  93. package/dist/hcl/roots.js +5 -0
  94. package/dist/hcl/suppressions.d.ts.map +1 -1
  95. package/dist/hcl/suppressions.js +8 -2
  96. package/dist/index.d.ts +11 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +21 -2
  99. package/dist/integrity.json +3 -3
  100. package/dist/live-plan-reasons.d.ts +21 -0
  101. package/dist/live-plan-reasons.d.ts.map +1 -0
  102. package/dist/live-plan-reasons.js +47 -0
  103. package/dist/lsp/option-keys.d.ts.map +1 -1
  104. package/dist/lsp/option-keys.js +2 -0
  105. package/dist/manifest.json +1 -1
  106. package/dist/op/activities/index.d.ts +6 -2
  107. package/dist/op/activities/index.d.ts.map +1 -1
  108. package/dist/op/activities/index.js +5 -1
  109. package/dist/op/activities/live-detect.d.ts +4 -3
  110. package/dist/op/activities/live-detect.d.ts.map +1 -1
  111. package/dist/op/activities/live-detect.js +57 -32
  112. package/dist/op/activities/pin-rollout.d.ts +2 -0
  113. package/dist/op/activities/pin-rollout.d.ts.map +1 -1
  114. package/dist/op/activities/pin-rollout.js +1 -0
  115. package/dist/op/activities/terraform.d.ts +153 -5
  116. package/dist/op/activities/terraform.d.ts.map +1 -1
  117. package/dist/op/activities/terraform.js +205 -20
  118. package/dist/op/activity-contracts.d.ts +27 -0
  119. package/dist/op/activity-contracts.d.ts.map +1 -1
  120. package/dist/op/activity-contracts.js +19 -1
  121. package/dist/op/builders.d.ts +7 -1
  122. package/dist/op/builders.d.ts.map +1 -1
  123. package/dist/op/builders.js +9 -0
  124. package/dist/pin/edit.d.ts.map +1 -1
  125. package/dist/pin/edit.js +35 -0
  126. package/dist/pin/rollout.d.ts +2 -0
  127. package/dist/pin/rollout.d.ts.map +1 -1
  128. package/dist/pin/rollout.js +14 -11
  129. package/dist/plan-shape.d.ts +30 -0
  130. package/dist/plan-shape.d.ts.map +1 -0
  131. package/dist/plan-shape.js +125 -0
  132. package/dist/plugin.d.ts.map +1 -1
  133. package/dist/plugin.js +32 -18
  134. package/dist/report.d.ts +15 -6
  135. package/dist/report.d.ts.map +1 -1
  136. package/dist/report.js +28 -55
  137. package/dist/skills/chant-terraform.md +1 -1
  138. package/dist/state/azure-blob.d.ts +68 -0
  139. package/dist/state/azure-blob.d.ts.map +1 -0
  140. package/dist/state/azure-blob.js +201 -0
  141. package/dist/state/backend.d.ts +158 -0
  142. package/dist/state/backend.d.ts.map +1 -0
  143. package/dist/state/backend.js +406 -0
  144. package/dist/state/exec.d.ts +39 -0
  145. package/dist/state/exec.d.ts.map +1 -0
  146. package/dist/state/exec.js +80 -0
  147. package/dist/state/gcs.d.ts +62 -0
  148. package/dist/state/gcs.d.ts.map +1 -0
  149. package/dist/state/gcs.js +224 -0
  150. package/dist/state/index.d.ts +15 -0
  151. package/dist/state/index.d.ts.map +1 -0
  152. package/dist/state/index.js +14 -0
  153. package/dist/state/locks.d.ts +105 -0
  154. package/dist/state/locks.d.ts.map +1 -0
  155. package/dist/state/locks.js +130 -0
  156. package/dist/state/object-store.d.ts +37 -0
  157. package/dist/state/object-store.d.ts.map +1 -0
  158. package/dist/state/object-store.js +20 -0
  159. package/dist/state/root-states.d.ts +131 -0
  160. package/dist/state/root-states.d.ts.map +1 -0
  161. package/dist/state/root-states.js +241 -0
  162. package/dist/state/s3.d.ts +68 -0
  163. package/dist/state/s3.d.ts.map +1 -0
  164. package/dist/state/s3.js +170 -0
  165. package/dist/state/store.d.ts +51 -0
  166. package/dist/state/store.d.ts.map +1 -0
  167. package/dist/state/store.js +185 -0
  168. package/dist/terragrunt/affected.d.ts.map +1 -1
  169. package/dist/terragrunt/affected.js +2 -1
  170. package/package.json +7 -3
  171. package/src/__fixtures__/ACCEPTANCE.md +69 -7
  172. package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
  173. package/src/__fixtures__/compose/state-app.json +66 -0
  174. package/src/__fixtures__/compose/state-edge.json +40 -0
  175. package/src/__fixtures__/compose/state-net.json +58 -0
  176. package/src/__fixtures__/compose/three-app.plan.json +184 -0
  177. package/src/__fixtures__/compose/three-composed.plan.json +509 -0
  178. package/src/__fixtures__/compose/three-edge.plan.json +144 -0
  179. package/src/__fixtures__/compose/three-net.plan.json +174 -0
  180. package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
  181. package/src/__fixtures__/compose/working-after-apply.json +145 -0
  182. package/src/__fixtures__/config-blocks/app/main.tf +61 -0
  183. package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
  184. package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
  185. package/src/__fixtures__/config-files/shadow/main.tf +3 -0
  186. package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
  187. package/src/__fixtures__/config-files/shadow/other.tf +3 -0
  188. package/src/__fixtures__/config-files/tf/main.tf +11 -0
  189. package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
  190. package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
  191. package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
  192. package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
  193. package/src/__fixtures__/drift-record/README.md +11 -0
  194. package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
  195. package/src/__fixtures__/instance-forms/app/main.tf +110 -0
  196. package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
  197. package/src/__fixtures__/instance-vars/app/main.tf +52 -0
  198. package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
  199. package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
  200. package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
  201. package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
  202. package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
  203. package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
  204. package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
  205. package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
  206. package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
  207. package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
  208. package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
  209. package/src/__fixtures__/pr-loop/README.md +50 -0
  210. package/src/__fixtures__/pr-loop/chant.config.json +13 -0
  211. package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
  212. package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
  213. package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
  214. package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
  215. package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
  216. package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
  217. package/src/__fixtures__/report/applied.show.json +51 -0
  218. package/src/__fixtures__/report/generate.ts +63 -0
  219. package/src/__fixtures__/report/plan.show.json +200 -0
  220. package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
  221. package/src/__fixtures__/report/tf-apply/report.json +338 -0
  222. package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
  223. package/src/__fixtures__/report/tf-plan/report.json +320 -0
  224. package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
  225. package/src/applied-state.ts +52 -0
  226. package/src/backend-config.test.ts +105 -0
  227. package/src/backend-config.ts +133 -0
  228. package/src/behaviour/kinds.ts +4 -0
  229. package/src/behaviour/predict.ts +5 -2
  230. package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
  231. package/src/behaviour/reference-catalog.ts +16 -5
  232. package/src/behaviour/request.ts +3 -2
  233. package/src/change-set-instances.test.ts +98 -0
  234. package/src/changed-roots.ts +14 -16
  235. package/src/commands.ts +4 -3
  236. package/src/components/capability-plugin.test.ts +172 -5
  237. package/src/components/capability-plugin.ts +212 -11
  238. package/src/components/change-set-graph.e2e.test.ts +11 -5
  239. package/src/components/compose-group.test.ts +486 -0
  240. package/src/components/compose-group.ts +489 -0
  241. package/src/components/demo-composed-roots.e2e.test.ts +440 -0
  242. package/src/components/member-outputs.test.ts +76 -0
  243. package/src/components/placeholder-preview.test.ts +147 -0
  244. package/src/components/placeholder-preview.ts +257 -0
  245. package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
  246. package/src/components/pr-loop.tofu.test.ts +112 -56
  247. package/src/compose/apply-events.test.ts +62 -0
  248. package/src/compose/apply-events.ts +100 -0
  249. package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
  250. package/src/compose/estate.test.ts +421 -0
  251. package/src/compose/estate.ts +443 -0
  252. package/src/compose/hcl.ts +311 -0
  253. package/src/compose/module-address.ts +49 -0
  254. package/src/compose/root.test.ts +416 -0
  255. package/src/compose/root.ts +592 -0
  256. package/src/compose/run.test.ts +440 -0
  257. package/src/compose/run.tofu.test.ts +176 -0
  258. package/src/compose/run.ts +527 -0
  259. package/src/compose/slice.test.ts +184 -0
  260. package/src/compose/slice.ts +290 -0
  261. package/src/compose/state-address.ts +157 -0
  262. package/src/compose/state-merge.test.ts +131 -0
  263. package/src/compose/state-merge.ts +148 -0
  264. package/src/compose.test.ts +248 -0
  265. package/src/compose.ts +627 -0
  266. package/src/composites/terraform-apply-op.test.ts +9 -0
  267. package/src/composites/terraform-apply-op.ts +7 -3
  268. package/src/config-file-listers.test.ts +265 -0
  269. package/src/config.ts +44 -9
  270. package/src/describe-resources.engine-facts.test.ts +412 -0
  271. package/src/describe-resources.live-module-keyed.test.ts +290 -0
  272. package/src/describe-resources.ts +416 -70
  273. package/src/drift-read.ts +284 -0
  274. package/src/drift-record.test.ts +223 -0
  275. package/src/drift-record.ts +455 -0
  276. package/src/hcl/config-blocks.test.ts +174 -0
  277. package/src/hcl/edges.ts +21 -1
  278. package/src/hcl/files.ts +234 -0
  279. package/src/hcl/instance-forms.test.ts +94 -0
  280. package/src/hcl/instances.test.ts +137 -0
  281. package/src/hcl/instances.ts +422 -0
  282. package/src/hcl/parse.test.ts +139 -1
  283. package/src/hcl/parse.ts +149 -16
  284. package/src/hcl/roots.ts +6 -0
  285. package/src/hcl/suppressions.ts +8 -2
  286. package/src/index.ts +76 -0
  287. package/src/live-plan-reasons.ts +54 -0
  288. package/src/lsp/completions.test.ts +1 -1
  289. package/src/lsp/option-keys.ts +2 -0
  290. package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
  291. package/src/op/activities/index.ts +9 -0
  292. package/src/op/activities/live-detect.ts +55 -26
  293. package/src/op/activities/pin-rollout.ts +3 -0
  294. package/src/op/activities/registry.test.ts +2 -1
  295. package/src/op/activities/terraform.test.ts +55 -0
  296. package/src/op/activities/terraform.ts +266 -22
  297. package/src/op/activity-contracts.test.ts +1 -0
  298. package/src/op/activity-contracts.ts +24 -1
  299. package/src/op/builders.ts +14 -0
  300. package/src/pin/bundle.test.ts +2 -0
  301. package/src/pin/edit.ts +30 -0
  302. package/src/pin/rollout.ts +16 -11
  303. package/src/plan-shape.test.ts +148 -0
  304. package/src/plan-shape.ts +128 -0
  305. package/src/plugin.ts +33 -19
  306. package/src/report.test.ts +82 -1
  307. package/src/report.ts +31 -54
  308. package/src/signal-only-root.test.ts +72 -0
  309. package/src/skills/chant-terraform.md +1 -1
  310. package/src/state/azure-blob.ts +223 -0
  311. package/src/state/backend-verdict.test.ts +187 -0
  312. package/src/state/backend.test.ts +127 -0
  313. package/src/state/backend.ts +488 -0
  314. package/src/state/exec.ts +111 -0
  315. package/src/state/gcs.ts +254 -0
  316. package/src/state/index.ts +15 -0
  317. package/src/state/locks.test.ts +84 -0
  318. package/src/state/locks.ts +201 -0
  319. package/src/state/object-store.ts +35 -0
  320. package/src/state/root-states-floci.e2e.test.ts +66 -0
  321. package/src/state/root-states.test.ts +462 -0
  322. package/src/state/root-states.ts +309 -0
  323. package/src/state/s3.ts +193 -0
  324. package/src/state/store.ts +225 -0
  325. package/src/terragrunt/affected.ts +2 -1
  326. package/src/workspace-kinds.test.ts +4 -17
  327. package/workspace-kinds.json +1 -24
  328. package/dist/workspace-member.d.ts +0 -30
  329. package/dist/workspace-member.d.ts.map +0 -1
  330. package/dist/workspace-member.js +0 -91
  331. package/schemas/pulumi.schema.json +0 -34
  332. package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
  333. package/src/testdata/pulumi-member/stack-export.json +0 -53
  334. package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
  335. package/src/workspace-member.test.ts +0 -89
  336. package/src/workspace-member.ts +0 -126
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Every file lister in the lexicon reads the same configuration files for one
3
+ * root, through `./hcl/files.ts` (chant#4457): `.tf` and `.tf.json`, plus
4
+ * `.tofu` and `.tofu.json` when the root runs tofu or choudoufu, with `x.tofu`
5
+ * shadowing `x.tf`. One test per lister that changed. The fast `live` readers
6
+ * also read a `.tf.json` file's `live` block (chant#4504).
7
+ */
8
+ import { execFileSync } from "node:child_process";
9
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { afterEach, describe, expect, test } from "vitest";
13
+ import { loadHcl2json } from "@intentius/chant/terraform/hcl2json";
14
+ import { listConfigFiles, readsTofuFiles, selectConfigFiles } from "./hcl/files";
15
+ import { configFiles, liveEstate, localModules, rootState } from "./compose/hcl";
16
+ import { hasCloudBlock, stateObject } from "./state/backend";
17
+ import { estateOfDir } from "./report";
18
+ import { detectLiveEstate, detectLivePolicyVerbs } from "./op/activities/live-detect";
19
+ import { rootWatchPaths } from "./changed-roots";
20
+ import { editPins } from "./pin/edit";
21
+ import { runPinRollout } from "./pin/rollout";
22
+ import { terragruntAffected } from "./terragrunt/affected";
23
+
24
+ const made: string[] = [];
25
+ afterEach(() => {
26
+ for (const d of made.splice(0)) rmSync(d, { recursive: true, force: true });
27
+ });
28
+
29
+ /** A temporary directory holding `files`, by path relative to it. */
30
+ function tree(files: Record<string, string>): string {
31
+ const dir = mkdtempSync(join(tmpdir(), "chant-4457-"));
32
+ made.push(dir);
33
+ for (const [path, text] of Object.entries(files)) {
34
+ mkdirSync(join(dir, path, ".."), { recursive: true });
35
+ writeFileSync(join(dir, path), text);
36
+ }
37
+ return dir;
38
+ }
39
+
40
+ describe("hcl/files: the one lister", () => {
41
+ test("which binaries read .tofu files", () => {
42
+ expect(readsTofuFiles(undefined)).toBe(false);
43
+ expect(readsTofuFiles("terraform")).toBe(false);
44
+ expect(readsTofuFiles("/opt/bin/terraform")).toBe(false);
45
+ expect(readsTofuFiles("tofu")).toBe(true);
46
+ expect(readsTofuFiles("/usr/local/bin/tofu")).toBe(true);
47
+ expect(readsTofuFiles("choudoufu")).toBe(true);
48
+ });
49
+
50
+ test("the file set follows the binary, and x.tofu shadows x.tf", () => {
51
+ const names = ["main.tf", "main.tofu", "extra.tf.json", "extra.tofu.json", "vars.tf", "notes.md", "terraform.tfvars", "estate.chdf.hcl"];
52
+ expect(selectConfigFiles(names)).toEqual(["extra.tf.json", "main.tf", "vars.tf"]);
53
+ expect(selectConfigFiles(names, "terraform")).toEqual(["extra.tf.json", "main.tf", "vars.tf"]);
54
+ expect(selectConfigFiles(names, "tofu")).toEqual(["extra.tofu.json", "main.tofu", "vars.tf"]);
55
+ expect(selectConfigFiles(names, "choudoufu")).toEqual(["extra.tofu.json", "main.tofu", "vars.tf"]);
56
+ });
57
+
58
+ test("a directory is listed by its files only, not a directory named like one", () => {
59
+ const dir = tree({ "main.tf": "", "a.tofu": "", "odd.tf/inner.txt": "" });
60
+ expect(listConfigFiles(dir)).toEqual(["main.tf"]);
61
+ expect(listConfigFiles(dir, "tofu")).toEqual(["a.tofu", "main.tf"]);
62
+ });
63
+ });
64
+
65
+ describe("compose/hcl reads the lister's set", () => {
66
+ test("configFiles follows the binary", () => {
67
+ const dir = tree({ "main.tf": "", "main.tofu": "", "x.tf.json": "{}" });
68
+ expect(configFiles(dir).map((p) => p.slice(dir.length + 1))).toEqual(["main.tf", "x.tf.json"]);
69
+ expect(configFiles(dir, "tofu").map((p) => p.slice(dir.length + 1))).toEqual(["main.tofu", "x.tf.json"]);
70
+ });
71
+
72
+ test("a backend in a .tofu file is seen under tofu only", () => {
73
+ const dir = tree({ "backend.tofu": 'terraform {\n backend "gcs" {\n bucket = "b"\n prefix = "p"\n }\n}\n' });
74
+ expect(rootState(dir, "app", { env: {}, recorded: null }).own).toEqual({ key: "local:app/terraform.tfstate" });
75
+ expect(rootState(dir, "app", { env: {}, recorded: null, binary: "tofu" }).own).toEqual({ bucket: "b", key: "p" });
76
+ });
77
+
78
+ test("a live block in a .tofu file is a choudoufu root's estate", () => {
79
+ const dir = tree({ "main.tofu": 'terraform {\n live {\n estate = "prod"\n }\n}\n' });
80
+ expect(liveEstate(dir)).toEqual({ live: true, estate: "prod" });
81
+ expect(liveEstate(dir, "terraform")).toEqual({ live: false });
82
+ });
83
+
84
+ test("a local module called from a .tofu file is followed under tofu", () => {
85
+ const dir = tree({ "root/main.tofu": 'module "net" {\n source = "../net"\n}\n', "net/main.tf": "" });
86
+ expect(localModules(join(dir, "root"))).toEqual([]);
87
+ expect(localModules(join(dir, "root"), "tofu")).toEqual([join(dir, "net")]);
88
+ });
89
+ });
90
+
91
+ describe("state/backend reads the lister's set", () => {
92
+ test("a cloud block in a .tofu file is refused under tofu, and unseen under terraform", () => {
93
+ const dir = tree({ "main.tofu": 'terraform {\n cloud {\n organization = "o"\n }\n}\n' });
94
+ expect(hasCloudBlock(dir)).toBe(false);
95
+ expect(hasCloudBlock(dir, "tofu")).toBe(true);
96
+ expect(hasCloudBlock(dir, "choudoufu")).toBe(true);
97
+ expect(stateObject(dir, {}, undefined, "tofu")).toMatchObject({ refused: "cloud" });
98
+ expect(stateObject(dir, {}, undefined, "terraform")).toMatchObject({ backend: "local" });
99
+ });
100
+
101
+ test("a cloud block in a .tofu.json file is seen under tofu", () => {
102
+ const dir = tree({ "main.tofu.json": JSON.stringify({ terraform: { cloud: { organization: "o" } } }) });
103
+ expect(hasCloudBlock(dir, "tofu")).toBe(true);
104
+ });
105
+ });
106
+
107
+ describe("report and live-detect read choudoufu's set", () => {
108
+ const live = 'terraform {\n live {\n estate = "prod"\n }\n}\n';
109
+
110
+ test("estateOfDir finds a live block in a .tofu file", () => {
111
+ expect(estateOfDir(tree({ "main.tofu": live }))).toBe("prod");
112
+ expect(estateOfDir(tree({ "main.tofu": live }), "terraform")).toBeUndefined();
113
+ });
114
+
115
+ test("detectLiveEstate finds a live block in a .tofu file, and x.tofu shadows x.tf", () => {
116
+ expect(detectLiveEstate(tree({ "main.tofu": live }))).toBe("prod");
117
+ expect(detectLiveEstate(tree({ "main.tf": live, "main.tofu": "" }))).toBeUndefined();
118
+ expect(detectLiveEstate(tree({ "main.tf": live, "main.tofu": "" }), "terraform")).toBe("prod");
119
+ });
120
+
121
+ const liveJson = JSON.stringify({ terraform: { required_version: ">= 1.5.0", live: { estate: "prod" } } });
122
+
123
+ test("estateOfDir finds a live block in a .tf.json file (chant#4504)", () => {
124
+ expect(estateOfDir(tree({ "main.tf.json": liveJson }))).toBe("prod");
125
+ expect(estateOfDir(tree({ "main.tf.json": liveJson }), "terraform")).toBe("prod");
126
+ // JSON syntax may write the terraform block as an array of bodies.
127
+ expect(estateOfDir(tree({ "main.tf.json": JSON.stringify({ terraform: [{}, { live: [{ estate: "second" }] }] }) }))).toBe("second");
128
+ expect(estateOfDir(tree({ "main.tf.json": JSON.stringify({ terraform: { live: { estate: "" } } }) }))).toBeUndefined();
129
+ expect(estateOfDir(tree({ "main.tf.json": "{ not json" }))).toBeUndefined();
130
+ // A live key outside the terraform block is not a live block.
131
+ expect(estateOfDir(tree({ "main.tf.json": JSON.stringify({ live: { estate: "prod" } }) }))).toBeUndefined();
132
+ });
133
+
134
+ test("estateOfDir takes the first live block in filename order across .tf and .tf.json", () => {
135
+ expect(estateOfDir(tree({ "a.tf.json": liveJson, "b.tf": live.replace("prod", "other") }))).toBe("prod");
136
+ expect(estateOfDir(tree({ "a.tf": live.replace("prod", "other"), "b.tf.json": liveJson }))).toBe("other");
137
+ });
138
+
139
+ test("estateOfDir reads a .tofu.json live block under choudoufu, and x.tofu.json shadows x.tf.json", () => {
140
+ expect(estateOfDir(tree({ "main.tofu.json": liveJson }))).toBe("prod");
141
+ expect(estateOfDir(tree({ "main.tofu.json": liveJson }), "terraform")).toBeUndefined();
142
+ expect(estateOfDir(tree({ "main.tf.json": liveJson, "main.tofu.json": "{}" }))).toBeUndefined();
143
+ });
144
+
145
+ test("detectLiveEstate finds a live block in a .tf.json file (chant#4504)", () => {
146
+ expect(detectLiveEstate(tree({ "main.tf.json": liveJson }))).toBe("prod");
147
+ expect(detectLiveEstate(tree({ "main.tf.json": liveJson }), "terraform")).toBe("prod");
148
+ expect(detectLiveEstate(tree({ "main.tofu.json": liveJson }))).toBe("prod");
149
+ expect(detectLiveEstate(tree({ "main.tf.json": JSON.stringify({ terraform: { live: { estate: "" } } }) }))).toBeUndefined();
150
+ expect(detectLiveEstate(tree({ "main.tf.json": "{ not json" }))).toBeUndefined();
151
+ // The sidecar still comes first.
152
+ expect(detectLiveEstate(tree({ "estate.chdf.hcl": 'estate = "side"\n', "main.tf.json": liveJson }))).toBe("side");
153
+ });
154
+
155
+ test("detectLivePolicyVerbs reads a .tf.json live block's policy, as an object or an array", () => {
156
+ const policy = { undeclared_tagged: "adopt", undeclared_untagged: "report" };
157
+ expect(detectLivePolicyVerbs(tree({ "main.tf.json": JSON.stringify({ terraform: { live: { estate: "prod", policy } } }) }))).toEqual({
158
+ undeclaredTagged: "adopt",
159
+ undeclaredUntagged: "report",
160
+ });
161
+ expect(detectLivePolicyVerbs(tree({ "main.tf.json": JSON.stringify({ terraform: { live: { estate: "prod", policy: [{ undeclared_untagged: "delete" }] } } }) }))).toEqual({
162
+ undeclaredTagged: undefined,
163
+ undeclaredUntagged: "delete",
164
+ });
165
+ expect(detectLivePolicyVerbs(tree({ "main.tf.json": liveJson }))).toBeUndefined();
166
+ });
167
+ });
168
+
169
+ describe("pin rollout's listers follow the binary (chant#4504)", () => {
170
+ const REGISTRY = "app.terraform.io/acme/vpc/aws";
171
+ const call = (version: string) => `module "vpc" {\n source = "${REGISTRY}"\n version = "${version}"\n}\n`;
172
+
173
+ /** A git repository on `main` holding `files`, committed. */
174
+ function repo(files: Record<string, string>): string {
175
+ const dir = tree(files);
176
+ const git = (...args: string[]) =>
177
+ execFileSync("git", args, { cwd: dir, env: { ...process.env, GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@t" } });
178
+ git("init", "--quiet", "--initial-branch=main");
179
+ git("add", ".");
180
+ git("commit", "--quiet", "-m", "fixture");
181
+ return dir;
182
+ }
183
+
184
+ async function roots(cwd: string, binary?: string) {
185
+ const r = await runPinRollout({ cwd, module: REGISTRY, from: "1.3.0", to: "1.4.0", parser: await loadHcl2json(), mode: "report", ...(binary ? { binary } : {}) });
186
+ return Object.fromEntries(r.roots.map((x) => [x.root, { state: x.state, files: x.files }]));
187
+ }
188
+
189
+ test("candidateRoots and rootFiles read .tofu files under tofu only, with x.tofu shadowing x.tf", async () => {
190
+ const cwd = repo({
191
+ "only-tofu/main.tofu": call("1.3.0"),
192
+ "shadowed/main.tf": call("1.2.0"),
193
+ "shadowed/main.tofu": call("1.3.0"),
194
+ "plain/main.tf": call("1.3.0"),
195
+ "plain/notes.md": call("1.3.0"),
196
+ });
197
+ expect(await roots(cwd)).toEqual({
198
+ plain: { state: "from", files: ["plain/main.tf"] },
199
+ shadowed: { state: "elsewhere", files: ["shadowed/main.tf"] },
200
+ });
201
+ expect(await roots(cwd, "tofu")).toEqual({
202
+ "only-tofu": { state: "from", files: ["only-tofu/main.tofu"] },
203
+ plain: { state: "from", files: ["plain/main.tf"] },
204
+ shadowed: { state: "from", files: ["shadowed/main.tofu"] },
205
+ });
206
+ });
207
+
208
+ test("a .tf.json root is a candidate, and its moving call is refused by name", async () => {
209
+ const cwd = repo({ "json/main.tf.json": JSON.stringify({ module: { vpc: { source: REGISTRY, version: "1.3.0" } } }) });
210
+ const r = await runPinRollout({ cwd, module: REGISTRY, from: "1.3.0", to: "1.4.0", parser: await loadHcl2json(), mode: "report" });
211
+ expect(r.roots).toEqual([expect.objectContaining({ root: "json", state: "refused", files: ["json/main.tf.json"], reason: expect.stringContaining("JSON syntax") })]);
212
+ expect(r.waves).toEqual([]);
213
+ });
214
+ });
215
+
216
+ describe("terragrunt affected follows a module's .tofu calls (chant#4504)", () => {
217
+ const unit = { path: "live/app", dependencies: [], reading: ["live/app/terragrunt.hcl", "modules/app/main.tofu"] };
218
+
219
+ test("a change under a module called from a .tofu file is a module-call reason", () => {
220
+ const dir = tree({
221
+ "live/app/terragrunt.hcl": 'terraform {\n source = "../../modules/app"\n}\n',
222
+ "modules/app/main.tofu": 'module "net" {\n source = "../net"\n}\n',
223
+ "modules/net/main.tf": "",
224
+ });
225
+ expect(terragruntAffected({ dir, units: [unit], terragruntSelected: [], changedFiles: ["modules/net/main.tf"] })).toEqual({
226
+ units: [{ path: "live/app", reasons: [{ kind: "module-call", files: ["modules/net/main.tf"], via: "modules/net" }] }],
227
+ notes: [],
228
+ });
229
+ });
230
+
231
+ test("a call in an x.tf that x.tofu shadows is not followed", () => {
232
+ const dir = tree({
233
+ "live/app/terragrunt.hcl": 'terraform {\n source = "../../modules/app"\n}\n',
234
+ "modules/app/main.tofu": "",
235
+ "modules/app/main.tf": 'module "old" {\n source = "../old"\n}\n',
236
+ "modules/old/main.tf": "",
237
+ });
238
+ expect(terragruntAffected({ dir, units: [unit], terragruntSelected: [], changedFiles: ["modules/old/main.tf"] })).toEqual({ units: [], notes: [] });
239
+ });
240
+ });
241
+
242
+ describe("changed-roots reads the lister's set", () => {
243
+ test("a module called from a .tofu file is watched for a tofu root only", () => {
244
+ const dir = tree({ "app/main.tofu": 'module "net" {\n source = "../modules/net"\n}\n', "modules/net/main.tf": "" });
245
+ expect(rootWatchPaths(dir, { dir: "app" })).toEqual(["app"]);
246
+ expect(rootWatchPaths(dir, { dir: "app" }, "tofu")).toEqual(["app", "modules/net"]);
247
+ });
248
+ });
249
+
250
+ describe("pin edit on a JSON-syntax file", () => {
251
+ const REGISTRY = "app.terraform.io/acme/vpc/aws";
252
+
253
+ test("a call that would move is refused by name, and the file is left as written", async () => {
254
+ const text = JSON.stringify({ module: { vpc: { source: REGISTRY, version: "1.3.0" } } });
255
+ const result = await editPins(text, "main.tf.json", { module: REGISTRY, from: "1.3.0", to: "1.4.0" }, await loadHcl2json());
256
+ expect(result.content).toBe(text);
257
+ expect(result.calls).toEqual([expect.objectContaining({ outcome: "refused", file: "main.tf.json", call: "module.vpc" })]);
258
+ });
259
+
260
+ test("a call already at the new pin reads as already", async () => {
261
+ const text = JSON.stringify({ module: { vpc: { source: REGISTRY, version: "1.4.0" } } });
262
+ const result = await editPins(text, "main.tofu.json", { module: REGISTRY, from: "1.3.0", to: "1.4.0" }, await loadHcl2json());
263
+ expect(result.calls.map((c) => c.outcome)).toEqual(["already"]);
264
+ });
265
+ });
package/src/config.ts CHANGED
@@ -63,8 +63,15 @@ export const terraformRootSchema = z.strictObject({
63
63
  workspace: z.string().optional(),
64
64
  /** `-var-file` arguments, in order, relative to `dir`. */
65
65
  varFiles: z.array(z.string()).optional(),
66
- /** `-backend-config` key/value pairs handed to `init`. */
66
+ /** `-backend-config` key/value pairs handed to `init`, after {@link backendConfigFiles}. */
67
67
  backendConfig: z.record(z.string(), z.string()).optional(),
68
+ /**
69
+ * Backend-config files handed to `init` as `-backend-config=<path>`, in
70
+ * order and before the {@link backendConfig} pairs, relative to `dir`
71
+ * (chant#4459). The composition reads their `key = "value"` lines as it
72
+ * reads the pairs, to find a root's state before init.
73
+ */
74
+ backendConfigFiles: z.array(z.string()).optional(),
68
75
  /**
69
76
  * The choudoufu estate this root runs against, for a live root whose HCL
70
77
  * does not declare one of its own (#2479).
@@ -109,6 +116,16 @@ export const terraformRootSchema = z.strictObject({
109
116
  * every root, for one run.
110
117
  */
111
118
  observe: terraformObserveDepthSchema.optional(),
119
+ /**
120
+ * The root is declared only so a change to it brings this project into the
121
+ * change (#4484). A dns member that declares another cloud's root, so that
122
+ * an azure change reaches dns, sets it on that root. Change detection
123
+ * (`changedUnits`) still counts the root; the build and every live read
124
+ * skip it, so it is never drawn or read as this project's own: its entities
125
+ * are not built, and `describeResources`, drift and the owned-resource
126
+ * reads leave it out. An Op step that names the root still runs against it.
127
+ */
128
+ signalOnly: z.boolean().optional(),
112
129
  });
113
130
 
114
131
  export const terraformConfigSchema = z.strictObject({
@@ -145,19 +162,37 @@ export const terraformConfigSchema = z.strictObject({
145
162
  moduleRoot: z.string().optional(),
146
163
  /** The default {@link TerraformRootConfig.observe} for every root that sets none (#4358). */
147
164
  observe: terraformObserveDepthSchema.optional(),
148
- /** Named root modules. The name is the entity-key prefix, so keep it stable. */
149
- roots: z.record(z.string(), terraformRootSchema).default({}),
150
165
  /**
151
- * One pulumi workspace member read into the graph (#3179). `chant workspace
152
- * graph` writes this into the reader project it makes for a `pulumi` member;
153
- * it is not meant to be written by hand. `dir` is the member's directory,
154
- * absolute; `label` names the entities. The graph is the checked-in
155
- * `stack-export.json` there; the program is never run.
166
+ * Composed runs (#4419, #4438), off unless `enabled` is true. When on, the
167
+ * `terraform-apply` components a change reaches, and the ones they read,
168
+ * plan as one plan and apply as one apply of a composed root of `module`
169
+ * calls, so a value one root creates reaches its readers in that apply. A
170
+ * component that cannot compose plans wave by wave as without the setting,
171
+ * and the report says why (`compositionFor` in `compose.ts`).
172
+ *
173
+ * `max` caps how many roots compose; a change over it applies wholly in
174
+ * waves. Default 10.
156
175
  */
157
- pulumiMember: z.strictObject({ dir: z.string().min(1), label: z.string().optional() }).optional(),
176
+ compose: z
177
+ .strictObject({
178
+ enabled: z.boolean(),
179
+ max: z.number().int().min(2).optional(),
180
+ })
181
+ .optional(),
182
+ /** Named root modules. The name is the entity-key prefix, so keep it stable. */
183
+ roots: z.record(z.string(), terraformRootSchema).default({}),
158
184
  });
159
185
 
160
186
  export type TerraformRootConfig = z.infer<typeof terraformRootSchema>;
187
+
188
+ /**
189
+ * The roots a project owns: `roots` without the ones marked `signalOnly`
190
+ * (#4484), which only feed change detection. The build and the live reads
191
+ * take these; `changedUnits` takes every root.
192
+ */
193
+ export function ownRoots<R extends { signalOnly?: boolean }>(roots: Record<string, R> | undefined): Record<string, R> {
194
+ return Object.fromEntries(Object.entries(roots ?? {}).filter(([, r]) => r.signalOnly !== true));
195
+ }
161
196
  export type TerraformConfig = z.infer<typeof terraformConfigSchema>;
162
197
 
163
198
  declare module "@intentius/chant/config" {