@intentius/chant-lexicon-terraform 0.102.0 → 0.104.0

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 (270) hide show
  1. package/dist/__fixtures__/terragrunt/affected-cases.d.ts +23 -0
  2. package/dist/__fixtures__/terragrunt/affected-cases.d.ts.map +1 -0
  3. package/dist/__fixtures__/terragrunt/record.d.ts +17 -0
  4. package/dist/__fixtures__/terragrunt/record.d.ts.map +1 -0
  5. package/dist/change-set.d.ts +67 -0
  6. package/dist/change-set.d.ts.map +1 -0
  7. package/dist/changed-roots.d.ts +30 -0
  8. package/dist/changed-roots.d.ts.map +1 -0
  9. package/dist/commands.d.ts +26 -0
  10. package/dist/commands.d.ts.map +1 -0
  11. package/dist/components/capability-plugin.d.ts +54 -0
  12. package/dist/components/capability-plugin.d.ts.map +1 -0
  13. package/dist/composites/terraform-pin-rollout-op.d.ts +44 -0
  14. package/dist/composites/terraform-pin-rollout-op.d.ts.map +1 -0
  15. package/dist/hcl/descend.d.ts +1 -1
  16. package/dist/hcl/descend.d.ts.map +1 -1
  17. package/dist/hcl/edges.d.ts +2 -2
  18. package/dist/hcl/edges.d.ts.map +1 -1
  19. package/dist/hcl/parse.d.ts +29 -1
  20. package/dist/hcl/parse.d.ts.map +1 -1
  21. package/dist/hcl/roots.d.ts +1 -1
  22. package/dist/hcl/roots.d.ts.map +1 -1
  23. package/dist/hcl/suppressions.d.ts.map +1 -1
  24. package/dist/index.d.ts +4 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/integrity.json +13 -4
  27. package/dist/lint/audit-catalog.d.ts.map +1 -1
  28. package/dist/lint/post-synth/index.d.ts.map +1 -1
  29. package/dist/lint/post-synth/module-source.d.ts +27 -1
  30. package/dist/lint/post-synth/module-source.d.ts.map +1 -1
  31. package/dist/lint/post-synth/terragrunt.d.ts +37 -0
  32. package/dist/lint/post-synth/terragrunt.d.ts.map +1 -0
  33. package/dist/lint/post-synth/tf002.d.ts +3 -1
  34. package/dist/lint/post-synth/tf002.d.ts.map +1 -1
  35. package/dist/lint/post-synth/tf038.d.ts +22 -0
  36. package/dist/lint/post-synth/tf038.d.ts.map +1 -0
  37. package/dist/lint/post-synth/tf039.d.ts +23 -0
  38. package/dist/lint/post-synth/tf039.d.ts.map +1 -0
  39. package/dist/lint/post-synth/tf040.d.ts +30 -0
  40. package/dist/lint/post-synth/tf040.d.ts.map +1 -0
  41. package/dist/lint/post-synth/tf041.d.ts +20 -0
  42. package/dist/lint/post-synth/tf041.d.ts.map +1 -0
  43. package/dist/lint/post-synth/tf042.d.ts +18 -0
  44. package/dist/lint/post-synth/tf042.d.ts.map +1 -0
  45. package/dist/lint/post-synth/tf043.d.ts +17 -0
  46. package/dist/lint/post-synth/tf043.d.ts.map +1 -0
  47. package/dist/lint/post-synth/tf044.d.ts +17 -0
  48. package/dist/lint/post-synth/tf044.d.ts.map +1 -0
  49. package/dist/lint/post-synth/tf045.d.ts +22 -0
  50. package/dist/lint/post-synth/tf045.d.ts.map +1 -0
  51. package/dist/manifest.json +1 -1
  52. package/dist/okf/index.md +8 -0
  53. package/dist/okf/rules/TF038.md +11 -0
  54. package/dist/okf/rules/TF039.md +11 -0
  55. package/dist/okf/rules/TF040.md +11 -0
  56. package/dist/okf/rules/TF041.md +11 -0
  57. package/dist/okf/rules/TF042.md +11 -0
  58. package/dist/okf/rules/TF043.md +11 -0
  59. package/dist/okf/rules/TF044.md +11 -0
  60. package/dist/okf/rules/TF045.md +11 -0
  61. package/dist/op/activities/index.d.ts +4 -0
  62. package/dist/op/activities/index.d.ts.map +1 -1
  63. package/dist/op/activities/pin-rollout.d.ts +46 -0
  64. package/dist/op/activities/pin-rollout.d.ts.map +1 -0
  65. package/dist/op/activities/terraform.d.ts +24 -50
  66. package/dist/op/activities/terraform.d.ts.map +1 -1
  67. package/dist/op/activity-contracts.d.ts +48 -0
  68. package/dist/op/activity-contracts.d.ts.map +1 -1
  69. package/dist/op/builders.d.ts +9 -0
  70. package/dist/op/builders.d.ts.map +1 -1
  71. package/dist/pin/edit-ts.d.ts +21 -0
  72. package/dist/pin/edit-ts.d.ts.map +1 -0
  73. package/dist/pin/edit.d.ts +52 -0
  74. package/dist/pin/edit.d.ts.map +1 -0
  75. package/dist/pin/forge.d.ts +39 -0
  76. package/dist/pin/forge.d.ts.map +1 -0
  77. package/dist/pin/index.d.ts +22 -0
  78. package/dist/pin/index.d.ts.map +1 -0
  79. package/dist/pin/rollout.d.ts +116 -0
  80. package/dist/pin/rollout.d.ts.map +1 -0
  81. package/dist/pin/scan.d.ts +54 -0
  82. package/dist/pin/scan.d.ts.map +1 -0
  83. package/dist/pin/source.d.ts +93 -0
  84. package/dist/pin/source.d.ts.map +1 -0
  85. package/dist/pin/waves.d.ts +66 -0
  86. package/dist/pin/waves.d.ts.map +1 -0
  87. package/dist/plan-digest.d.ts +59 -0
  88. package/dist/plan-digest.d.ts.map +1 -0
  89. package/dist/plugin.d.ts.map +1 -1
  90. package/dist/rules/module-source.ts +56 -1
  91. package/dist/rules/terragrunt.ts +76 -0
  92. package/dist/rules/tf002.ts +1 -1
  93. package/dist/rules/tf038.ts +71 -0
  94. package/dist/rules/tf039.ts +64 -0
  95. package/dist/rules/tf040.ts +76 -0
  96. package/dist/rules/tf041.ts +56 -0
  97. package/dist/rules/tf042.ts +42 -0
  98. package/dist/rules/tf043.ts +49 -0
  99. package/dist/rules/tf044.ts +51 -0
  100. package/dist/rules/tf045.ts +47 -0
  101. package/dist/terragrunt/affected.d.ts +119 -0
  102. package/dist/terragrunt/affected.d.ts.map +1 -0
  103. package/dist/terragrunt/index.d.ts +11 -0
  104. package/dist/terragrunt/index.d.ts.map +1 -0
  105. package/dist/terragrunt/mocks.d.ts +147 -0
  106. package/dist/terragrunt/mocks.d.ts.map +1 -0
  107. package/dist/terragrunt/run.d.ts +130 -0
  108. package/dist/terragrunt/run.d.ts.map +1 -0
  109. package/dist/terragrunt/units.d.ts +103 -0
  110. package/dist/terragrunt/units.d.ts.map +1 -0
  111. package/dist/terragrunt/wave.d.ts +89 -0
  112. package/dist/terragrunt/wave.d.ts.map +1 -0
  113. package/package.json +12 -2
  114. package/src/__fixtures__/change-set/choudoufu-set-plan.change-set.golden.json +95 -0
  115. package/src/__fixtures__/change-set/choudoufu-set-plan.json +76 -0
  116. package/src/__fixtures__/change-set/choudoufu.part.golden.json +173 -0
  117. package/src/__fixtures__/change-set/choudoufu.plan.json +610 -0
  118. package/src/__fixtures__/change-set/terraform.import-forget.plan.json +81 -0
  119. package/src/__fixtures__/change-set/terraform.part.golden.json +173 -0
  120. package/src/__fixtures__/change-set/terraform.plan.json +589 -0
  121. package/src/__fixtures__/change-set/tofu.part.golden.json +173 -0
  122. package/src/__fixtures__/change-set/tofu.plan.json +588 -0
  123. package/src/__fixtures__/change-set/v1.tf.txt +21 -0
  124. package/src/__fixtures__/change-set/v2-module-app.tf.txt +4 -0
  125. package/src/__fixtures__/change-set/v2.tf.txt +21 -0
  126. package/src/__fixtures__/pin/choudoufu-live-waves-n5.json +83 -0
  127. package/src/__fixtures__/pin/largeset-oci-n5/apply-order.txt +5 -0
  128. package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/estate.chdf.hcl +6 -0
  129. package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/main.tf +33 -0
  130. package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/estate.chdf.hcl +6 -0
  131. package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/main.tf +45 -0
  132. package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/estate.chdf.hcl +6 -0
  133. package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/main.tf +47 -0
  134. package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/estate.chdf.hcl +6 -0
  135. package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/main.tf +31 -0
  136. package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/estate.chdf.hcl +6 -0
  137. package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/main.tf +27 -0
  138. package/src/__fixtures__/pin/largeset-oci-n5/fixture.json +77 -0
  139. package/src/__fixtures__/plan-summary/largeset-n5-bump.json +2071 -0
  140. package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.json +95 -0
  141. package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.md +22 -0
  142. package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.txt +12 -0
  143. package/src/__fixtures__/terragrunt/affected/catalog/units/web/terragrunt.hcl +3 -0
  144. package/src/__fixtures__/terragrunt/affected/gitignore +3 -0
  145. package/src/__fixtures__/terragrunt/affected/live/dev/app/terragrunt.hcl +3 -0
  146. package/src/__fixtures__/terragrunt/affected/live/dev/vpc/terragrunt.hcl +3 -0
  147. package/src/__fixtures__/terragrunt/affected/live/prod/app/terragrunt.hcl +3 -0
  148. package/src/__fixtures__/terragrunt/affected/live/stg/terragrunt.stack.hcl +5 -0
  149. package/src/__fixtures__/terragrunt/affected/modules/app/main.tf +6 -0
  150. package/src/__fixtures__/terragrunt/affected/modules/app/policy.json +1 -0
  151. package/src/__fixtures__/terragrunt/affected/modules/net/main.tf +2 -0
  152. package/src/__fixtures__/terragrunt/affected/modules/thing/main.tf +3 -0
  153. package/src/__fixtures__/terragrunt/affected/root.hcl +5 -0
  154. package/src/__fixtures__/terragrunt/affected-cases.ts +126 -0
  155. package/src/__fixtures__/terragrunt/five-units/catalog/units/thing/terragrunt.hcl +3 -0
  156. package/src/__fixtures__/terragrunt/five-units/live/dev/app/terragrunt.hcl +13 -0
  157. package/src/__fixtures__/terragrunt/five-units/live/dev/db/terragrunt.hcl +8 -0
  158. package/src/__fixtures__/terragrunt/five-units/live/dev/vpc/terragrunt.hcl +3 -0
  159. package/src/__fixtures__/terragrunt/five-units/live/prod/app/terragrunt.hcl +8 -0
  160. package/src/__fixtures__/terragrunt/five-units/live/prod/vpc/terragrunt.hcl +3 -0
  161. package/src/__fixtures__/terragrunt/five-units/modules/thing/main.tf +7 -0
  162. package/src/__fixtures__/terragrunt/five-units/root.hcl +5 -0
  163. package/src/__fixtures__/terragrunt/record.ts +56 -0
  164. package/src/__fixtures__/terragrunt/recorded/affected-find.json +50 -0
  165. package/src/__fixtures__/terragrunt/recorded/failed-plan-report.json +36 -0
  166. package/src/__fixtures__/terragrunt/recorded/find.json +67 -0
  167. package/src/__fixtures__/terragrunt/recorded/version.txt +1 -0
  168. package/src/__fixtures__/terragrunt/recorded/wave-1/apply-report.json +22 -0
  169. package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/dev/vpc/tfplan.json +1 -0
  170. package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/prod/vpc/tfplan.json +1 -0
  171. package/src/__fixtures__/terragrunt/recorded/wave-1/plan-report.json +22 -0
  172. package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/dev/db/tfplan.json +1 -0
  173. package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/prod/app/tfplan.json +1 -0
  174. package/src/__fixtures__/terragrunt/recorded/wave-2/plan-report.json +22 -0
  175. package/src/change-set-combined-run.test.ts +156 -0
  176. package/src/change-set.test.ts +151 -0
  177. package/src/change-set.ts +273 -0
  178. package/src/changed-roots.test.ts +58 -0
  179. package/src/changed-roots.ts +102 -0
  180. package/src/commands.ts +112 -0
  181. package/src/components/capability-plugin.test.ts +91 -0
  182. package/src/components/capability-plugin.ts +131 -0
  183. package/src/components/pr-loop.tofu.test.ts +244 -0
  184. package/src/composites/terraform-pin-rollout-op.test.ts +69 -0
  185. package/src/composites/terraform-pin-rollout-op.ts +60 -0
  186. package/src/hcl/descend.ts +1 -1
  187. package/src/hcl/edges.ts +2 -2
  188. package/src/hcl/parse.test.ts +25 -0
  189. package/src/hcl/parse.ts +91 -6
  190. package/src/hcl/roots.ts +1 -1
  191. package/src/hcl/suppressions.ts +8 -2
  192. package/src/index.ts +18 -0
  193. package/src/lint/audit-catalog.ts +82 -0
  194. package/src/lint/audit.test.ts +3 -1
  195. package/src/lint/post-synth/fixtures/TF038/positive.tf +3 -0
  196. package/src/lint/post-synth/fixtures/TF039/positive.tf +4 -0
  197. package/src/lint/post-synth/fixtures/TF041/negative/terragrunt.hcl +12 -0
  198. package/src/lint/post-synth/fixtures/TF041/positive/terragrunt.hcl +7 -0
  199. package/src/lint/post-synth/fixtures/TF041/positive-apply/terragrunt.hcl +8 -0
  200. package/src/lint/post-synth/fixtures/TF042/negative/terragrunt.hcl +13 -0
  201. package/src/lint/post-synth/fixtures/TF042/positive/terragrunt.hcl +9 -0
  202. package/src/lint/post-synth/fixtures/TF043/negative/terragrunt.hcl +3 -0
  203. package/src/lint/post-synth/fixtures/TF043/negative-other/terragrunt.hcl +3 -0
  204. package/src/lint/post-synth/fixtures/TF043/positive-git/terragrunt.hcl +3 -0
  205. package/src/lint/post-synth/fixtures/TF043/positive-oci/terragrunt.hcl +3 -0
  206. package/src/lint/post-synth/fixtures/TF043/positive-tfr/terragrunt.hcl +3 -0
  207. package/src/lint/post-synth/fixtures/TF044/negative/terragrunt.hcl +9 -0
  208. package/src/lint/post-synth/fixtures/TF044/positive/terragrunt.hcl +7 -0
  209. package/src/lint/post-synth/fixtures/TF044/positive-generate/terragrunt.hcl +9 -0
  210. package/src/lint/post-synth/fixtures/TF045/negative/root.hcl +1 -0
  211. package/src/lint/post-synth/fixtures/TF045/negative/terragrunt.hcl +3 -0
  212. package/src/lint/post-synth/fixtures/TF045/positive/root.hcl +9 -0
  213. package/src/lint/post-synth/fixtures/TF045/positive/terragrunt.hcl +3 -0
  214. package/src/lint/post-synth/fixtures/TF045/unit-only/terragrunt.hcl +3 -0
  215. package/src/lint/post-synth/index.ts +16 -0
  216. package/src/lint/post-synth/module-source.test.ts +42 -1
  217. package/src/lint/post-synth/module-source.ts +56 -1
  218. package/src/lint/post-synth/terragrunt.ts +76 -0
  219. package/src/lint/post-synth/tf002.ts +1 -1
  220. package/src/lint/post-synth/tf038.test.ts +51 -0
  221. package/src/lint/post-synth/tf038.ts +71 -0
  222. package/src/lint/post-synth/tf039.test.ts +40 -0
  223. package/src/lint/post-synth/tf039.ts +64 -0
  224. package/src/lint/post-synth/tf040.test.ts +55 -0
  225. package/src/lint/post-synth/tf040.ts +76 -0
  226. package/src/lint/post-synth/tf041.test.ts +50 -0
  227. package/src/lint/post-synth/tf041.ts +56 -0
  228. package/src/lint/post-synth/tf042.test.ts +19 -0
  229. package/src/lint/post-synth/tf042.ts +42 -0
  230. package/src/lint/post-synth/tf043.test.ts +65 -0
  231. package/src/lint/post-synth/tf043.ts +49 -0
  232. package/src/lint/post-synth/tf044.test.ts +27 -0
  233. package/src/lint/post-synth/tf044.ts +51 -0
  234. package/src/lint/post-synth/tf045.test.ts +30 -0
  235. package/src/lint/post-synth/tf045.ts +47 -0
  236. package/src/op/activities/index.ts +4 -0
  237. package/src/op/activities/pin-rollout.ts +77 -0
  238. package/src/op/activities/terraform.test.ts +13 -0
  239. package/src/op/activities/terraform.ts +46 -86
  240. package/src/op/activity-contracts.test.ts +4 -1
  241. package/src/op/activity-contracts.ts +42 -0
  242. package/src/op/builders.ts +13 -0
  243. package/src/pin/bundle.test.ts +53 -0
  244. package/src/pin/edit-ts.test.ts +39 -0
  245. package/src/pin/edit-ts.ts +77 -0
  246. package/src/pin/edit.test.ts +199 -0
  247. package/src/pin/edit.ts +157 -0
  248. package/src/pin/forge.ts +85 -0
  249. package/src/pin/index.ts +22 -0
  250. package/src/pin/rollout.test.ts +232 -0
  251. package/src/pin/rollout.ts +451 -0
  252. package/src/pin/scan.ts +309 -0
  253. package/src/pin/source.ts +206 -0
  254. package/src/pin/waves.test.ts +101 -0
  255. package/src/pin/waves.ts +125 -0
  256. package/src/plan-digest.ts +99 -0
  257. package/src/plan-summary.test.ts +154 -0
  258. package/src/plugin.ts +25 -2
  259. package/src/terragrunt/affected.acceptance.test.ts +83 -0
  260. package/src/terragrunt/affected.test.ts +177 -0
  261. package/src/terragrunt/affected.ts +348 -0
  262. package/src/terragrunt/index.ts +11 -0
  263. package/src/terragrunt/mocks.acceptance.test.ts +105 -0
  264. package/src/terragrunt/mocks.test.ts +309 -0
  265. package/src/terragrunt/mocks.ts +324 -0
  266. package/src/terragrunt/run.ts +369 -0
  267. package/src/terragrunt/terragrunt.acceptance.test.ts +122 -0
  268. package/src/terragrunt/terragrunt.test.ts +332 -0
  269. package/src/terragrunt/units.ts +235 -0
  270. package/src/terragrunt/wave.ts +181 -0
@@ -0,0 +1,59 @@
1
+ /**
2
+ * A terraform plan's identity (#2300), kept apart from the activities so the
3
+ * change-set adapter (`../change-set.ts`, #3181) can import it without the
4
+ * config loader, the HCL parse or anything else the activities reach. It
5
+ * imports chant's `lifecycle/plan-digest` subpath and nothing more, which is
6
+ * what lets terragucci bundle it into one file (#3421).
7
+ */
8
+ /**
9
+ * One resource's proposed change, as a plan digest sees it (#2300) — the
10
+ * projection of a `terraform show -json` `resource_changes` entry that
11
+ * survives into {@link terraformPlanDigest}.
12
+ */
13
+ export interface TerraformPlannedChange {
14
+ address: unknown;
15
+ mode: unknown;
16
+ type: unknown;
17
+ name: unknown;
18
+ index: unknown;
19
+ provider_name: unknown;
20
+ deposed: unknown;
21
+ actions: unknown;
22
+ before: unknown;
23
+ after: unknown;
24
+ after_unknown: unknown;
25
+ replace_paths: unknown;
26
+ importing: unknown;
27
+ }
28
+ /**
29
+ * The change set a `terraform show -json` plan describes, canonicalised
30
+ * (#2300): every proposed change, at its address, with the values that would
31
+ * be written, sorted so two renderings of the same plan project identically.
32
+ *
33
+ * Everything else in the document is dropped, and the drops are the point —
34
+ * see `packages/core/src/lifecycle/plan-digest.ts` for the general rule. Here
35
+ * that means `timestamp` and `terraform_version` (when the plan was taken and
36
+ * by which binary), `format_version` (how it was rendered), `prior_state` and
37
+ * `planned_values` (restatements of the same change set from another angle),
38
+ * `configuration` (the source that produced it, which can be refactored
39
+ * without changing a single proposed action) and `checks` / `relevant_
40
+ * attributes` (diagnostics about the plan, not the plan). What is left is
41
+ * what applying it would do.
42
+ */
43
+ export declare function terraformPlanChangeSet(planJson: unknown): {
44
+ resourceChanges: TerraformPlannedChange[];
45
+ outputChanges: unknown;
46
+ actionInvocations?: unknown[];
47
+ };
48
+ /**
49
+ * The identity of a terraform plan (#2300): {@link terraformPlanChangeSet}
50
+ * hashed. This is what a `TerraformApplyOp` gate binds its approval to, and
51
+ * what a later run recomputes and compares.
52
+ *
53
+ * Taken over the `show -json` rendering rather than the plan file's bytes on
54
+ * purpose: the file is an opaque, version-stamped archive, and two runs of
55
+ * `plan -out` over an unchanged root produce different bytes. The rendering
56
+ * is the same plan both times.
57
+ */
58
+ export declare function terraformPlanDigest(planJson: unknown): string;
59
+ //# sourceMappingURL=plan-digest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-digest.d.ts","sourceRoot":"","sources":["../src/plan-digest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,OAAO,GAAG;IACzD,eAAe,EAAE,sBAAsB,EAAE,CAAC;IAC1C,aAAa,EAAE,OAAO,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,EAAE,CAAC;CAC/B,CAkCA;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAE7D"}
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAgElF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,aAiQ7B,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAmElF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,aAqR7B,CAAC"}
@@ -18,14 +18,27 @@
18
18
  * recognize, scp-style `git@host:path` addresses, and any source ending in
19
19
  * `.git`. `local` is `./` or `../` (checkov's `UNKNOWN` case for module
20
20
  * pinning: a local path cannot be pinned to anything and is never flagged).
21
+ * `oci` is an OpenTofu `oci://host/repo` source (#3190). OpenTofu selects a
22
+ * version with `?tag=` or `?digest=` (https://opentofu.org/docs/language/modules/sources/),
23
+ * and a source naming neither resolves the `latest` tag. The container-image
24
+ * shapes `repo:tag` and `repo@sha256:...` are read too, so a source written
25
+ * the way `docker pull` spells it is not misread as unpinned. A `:` inside the
26
+ * host (a registry port) is never a tag: only a colon in the last path
27
+ * segment is. The tag and digest rules were checked against the docs page
28
+ * above; the suffix forms are not documented there and are not verified
29
+ * against OpenTofu's parser.
21
30
  */
22
31
 
23
- export type ModuleSourceKind = "local" | "registry" | "git" | "other";
32
+ export type ModuleSourceKind = "local" | "registry" | "git" | "oci" | "other";
24
33
 
25
34
  export interface ModuleSourceClassification {
26
35
  kind: ModuleSourceKind;
27
36
  /** The `?ref=`/`&ref=` value (falling back to `rev`), when `kind` is `"git"`. */
28
37
  ref?: string;
38
+ /** The tag an `oci://` source names, from `?tag=` or a `:tag` suffix on the repository. */
39
+ tag?: string;
40
+ /** The digest an `oci://` source names, from `?digest=` or an `@sha256:...` suffix. */
41
+ digest?: string;
29
42
  }
30
43
 
31
44
  const REGISTRY_NAME = /^[0-9A-Za-z](?:[0-9A-Za-z_-]{0,62}[0-9A-Za-z])?$/;
@@ -77,6 +90,25 @@ function isGitShaped(rest: string): boolean {
77
90
  return false;
78
91
  }
79
92
 
93
+ function classifyOci(base: string, query: string): ModuleSourceClassification {
94
+ const params = parseQuery(query);
95
+ let path = stripSubdir(base).replace(/^oci:\/\//i, "");
96
+ let tag = params.get("tag") || undefined;
97
+ let digest = params.get("digest") || undefined;
98
+
99
+ const at = path.indexOf("@");
100
+ if (at !== -1) {
101
+ digest = digest ?? (path.slice(at + 1) || undefined);
102
+ path = path.slice(0, at);
103
+ }
104
+ const lastSlash = path.lastIndexOf("/");
105
+ const colon = path.indexOf(":", lastSlash + 1); // a port sits before the first slash
106
+ if (colon !== -1) {
107
+ tag = tag ?? (path.slice(colon + 1) || undefined);
108
+ }
109
+ return { kind: "oci", ...(tag !== undefined ? { tag } : {}), ...(digest !== undefined ? { digest } : {}) };
110
+ }
111
+
80
112
  export function classifyModuleSource(source: string): ModuleSourceClassification {
81
113
  const trimmed = source.trim();
82
114
  if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith(".\\") || trimmed.startsWith("..\\")) {
@@ -87,6 +119,8 @@ export function classifyModuleSource(source: string): ModuleSourceClassification
87
119
  const base = queryIdx === -1 ? trimmed : trimmed.slice(0, queryIdx);
88
120
  const query = queryIdx === -1 ? "" : trimmed.slice(queryIdx + 1);
89
121
 
122
+ if (/^oci:\/\//i.test(base)) return classifyOci(base, query);
123
+
90
124
  const forced = /^(git|hg)::/i.exec(base);
91
125
  const rest = forced ? base.slice(forced[0].length) : base;
92
126
 
@@ -119,3 +153,24 @@ export function isPinnedRef(ref: string): boolean {
119
153
  export function isDefaultBranch(ref: string): boolean {
120
154
  return DEFAULT_BRANCHES.has(ref);
121
155
  }
156
+
157
+ const MUTABLE_TAGS = new Set(["latest", "main", "master", "develop", "dev", "stable", "edge", "nightly"]);
158
+
159
+ /**
160
+ * Is `tag` a name that conventionally moves (`latest`, a branch-like name)
161
+ * rather than an exact version. Used by TF038. A tag is a mutable pointer in
162
+ * most registries whatever it looks like, so the rule is a warning and this
163
+ * only decides what reads as obviously floating.
164
+ */
165
+ export function isMutableOciTag(tag: string): boolean {
166
+ return MUTABLE_TAGS.has(tag.toLowerCase());
167
+ }
168
+
169
+ /**
170
+ * Is a registry `version` constraint an exact version (`1.4.0`, `= 1.4.0`,
171
+ * `=1.4.0`) rather than a range (`~> 1.4`, `>= 1.4`, `>= 1.0, < 2.0`). Used
172
+ * by TF039. A bare version is an exact constraint in Terraform.
173
+ */
174
+ export function isExactVersionConstraint(version: string): boolean {
175
+ return /^(?:=\s*)?v?\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version.trim()) && !version.includes(",");
176
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Shared reading for the Terragrunt rules TF041 to TF045 (#3417).
3
+ *
4
+ * These rules look at `terragrunt.hcl` and `root.hcl`, which the parse reads
5
+ * into their own entity types (`TERRAGRUNT_*_TYPE` in `../../hcl/parse.ts`)
6
+ * and never into the module types the other TF rules check. Like the module
7
+ * source rules, they read the literal values hcl2json hands back: an
8
+ * attribute built from a function or a `local.` reference arrives as a
9
+ * `"${...}"` string and is treated as not determined, never as a finding.
10
+ */
11
+
12
+ import { isResourceDeclarable, type Declarable } from "@intentius/chant/declarable";
13
+ import type { BlockBody } from "../../hcl/parse";
14
+ import { classifyModuleSource } from "./module-source";
15
+
16
+ /** Entities of one Terragrunt type, with their keys, in map order. */
17
+ export function terragruntEntities(
18
+ entities: ReadonlyMap<string, Declarable>,
19
+ entityType: string,
20
+ ): Array<{ key: string; address: string; file: string; body: BlockBody }> {
21
+ const out: Array<{ key: string; address: string; file: string; body: BlockBody }> = [];
22
+ for (const [key, entity] of entities) {
23
+ if (entity.entityType !== entityType || !isResourceDeclarable(entity)) continue;
24
+ const props = entity.props as { address?: unknown; file?: unknown; body?: unknown };
25
+ out.push({
26
+ key,
27
+ address: typeof props.address === "string" ? props.address : key,
28
+ file: typeof props.file === "string" ? props.file : "terragrunt.hcl",
29
+ body: (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody,
30
+ });
31
+ }
32
+ return out;
33
+ }
34
+
35
+ /** An HCL expression hcl2json left as `"${...}"`: its value is not known without evaluating it. */
36
+ export function isExpression(value: unknown): boolean {
37
+ return typeof value === "string" && value.includes("${");
38
+ }
39
+
40
+ export type TerragruntSourceKind = "local" | "git" | "tfr" | "oci" | "other";
41
+
42
+ export interface TerragruntSourceClassification {
43
+ kind: TerragruntSourceKind;
44
+ /** Whether the source names a version: `?ref=` for git, `?version=` for `tfr://`, a tag or digest for `oci://`. Undefined for `local` and `other`. */
45
+ pinned?: boolean;
46
+ }
47
+
48
+ /**
49
+ * Classify a unit's `terraform.source`. Git and `oci://` sources go through
50
+ * {@link classifyModuleSource}, the reader TF005 and TF038 use. `tfr://` is
51
+ * Terragrunt's own scheme for registry modules and takes its version from the
52
+ * `?version=` query argument. A source that holds an interpolation, or that
53
+ * names an absolute path or an archive URL, is `other` or `local` and is
54
+ * never reported.
55
+ */
56
+ export function classifyTerragruntSource(source: string): TerragruntSourceClassification {
57
+ const trimmed = source.trim();
58
+ if (trimmed.includes("${")) return { kind: "other" };
59
+ if (/^tfr:\/\//i.test(trimmed)) {
60
+ const query = trimmed.includes("?") ? trimmed.slice(trimmed.indexOf("?") + 1) : "";
61
+ const version = new URLSearchParams(query).get("version");
62
+ return { kind: "tfr", pinned: version !== null && version.trim() !== "" };
63
+ }
64
+ if (trimmed.startsWith("/")) return { kind: "local" };
65
+ const classified = classifyModuleSource(trimmed);
66
+ switch (classified.kind) {
67
+ case "local":
68
+ return { kind: "local" };
69
+ case "git":
70
+ return { kind: "git", pinned: classified.ref !== undefined };
71
+ case "oci":
72
+ return { kind: "oci", pinned: classified.tag !== undefined || classified.digest !== undefined };
73
+ default:
74
+ return { kind: "other" };
75
+ }
76
+ }
@@ -51,7 +51,7 @@ function providerNameFromType(type: string): string {
51
51
  }
52
52
 
53
53
  /** One implied provider name to the entity key that first implied it in file order. */
54
- function impliedProviders(entities: PostSynthContext["entities"], root: string): Map<string, string> {
54
+ export function impliedProviders(entities: PostSynthContext["entities"], root: string): Map<string, string> {
55
55
  const implied = new Map<string, string>();
56
56
  const fromProviderBlock = new Set<string>();
57
57
 
@@ -0,0 +1,71 @@
1
+ /**
2
+ * TF038: an `oci://` module source names no tag and no digest, or a mutable
3
+ * tag.
4
+ *
5
+ * OpenTofu resolves an OCI module source with no `tag` to the `latest` tag
6
+ * (https://opentofu.org/docs/language/modules/sources/), so an unpinned source
7
+ * and `?tag=latest` are the same thing: a push to the registry changes what
8
+ * the root provisions with no diff to review. A digest, or an exact version
9
+ * tag, holds it. `classifyModuleSource` (`./module-source.ts`) reads the tag
10
+ * and digest from either spelling.
11
+ *
12
+ * A warning rather than an error: a registry can make a version tag
13
+ * immutable, and a source that names `?tag=1.4.0` is read as pinned here
14
+ * whether or not the registry enforces that. A digest is the only pin the
15
+ * registry cannot move, which the message says. A source that names a digest
16
+ * is never flagged, whatever tag sits beside it.
17
+ *
18
+ * Scope: root and child modules alike (#2112), same as TF004 and TF005.
19
+ */
20
+
21
+ import type {
22
+ PostSynthCheck,
23
+ PostSynthContext,
24
+ PostSynthDiagnostic,
25
+ } from "@intentius/chant/lint/post-synth";
26
+ import { isResourceDeclarable } from "@intentius/chant/declarable";
27
+ import { MODULE_TYPE, type BlockBody } from "../../hcl/parse";
28
+ import { classifyModuleSource, isMutableOciTag } from "./module-source";
29
+
30
+ export const tf038: PostSynthCheck = {
31
+ id: "TF038",
32
+ description: "OCI module source has no tag or digest, or a mutable tag",
33
+
34
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
35
+ const diagnostics: PostSynthDiagnostic[] = [];
36
+
37
+ for (const [key, entity] of ctx.entities) {
38
+ if (entity.entityType !== MODULE_TYPE || !isResourceDeclarable(entity)) continue;
39
+ const props = entity.props as { address?: unknown; body?: unknown };
40
+ const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
41
+ const source = typeof body.source === "string" ? body.source : undefined;
42
+ if (source === undefined) continue;
43
+
44
+ const classified = classifyModuleSource(source);
45
+ if (classified.kind !== "oci" || classified.digest) continue;
46
+
47
+ const address = typeof props.address === "string" ? props.address : key;
48
+ let message: string | undefined;
49
+ if (!classified.tag) {
50
+ message =
51
+ `Module "${address}" sources "${source}" with no tag or digest, so OpenTofu pulls the \`latest\` tag. ` +
52
+ "Pin it to a digest (`?digest=sha256:...`) or an exact version tag (`?tag=1.4.0`).";
53
+ } else if (isMutableOciTag(classified.tag)) {
54
+ message =
55
+ `Module "${address}" sources "${source}" with the mutable tag "${classified.tag}". ` +
56
+ "Pin it to a digest (`?digest=sha256:...`) or an exact version tag (`?tag=1.4.0`).";
57
+ }
58
+ if (!message) continue;
59
+
60
+ diagnostics.push({
61
+ checkId: "TF038",
62
+ severity: "warning",
63
+ message,
64
+ entity: key,
65
+ lexicon: "terraform",
66
+ });
67
+ }
68
+
69
+ return diagnostics;
70
+ },
71
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * TF039: a registry module's `version` is a range, not an exact version.
3
+ *
4
+ * TF004 flags a registry module with no `version`; a `version` of `~> 1.4`,
5
+ * `>= 1.4` or `>= 1.0, < 2.0` passes it, yet an upstream release inside the
6
+ * range changes what the root provisions with no diff to review. This rule is
7
+ * for projects that pin modules to one version (`1.4.0` or `= 1.4.0`).
8
+ *
9
+ * Off by default: it is a `report-only` catalog entry, so the `recommended`
10
+ * preset leaves it out and `lint.presets: { terraform: "all" }` (or a
11
+ * `lint.rules` entry for TF039) turns it on. Ranges are common and often
12
+ * intended, so a default warning would be noise for most roots. It is a
13
+ * separate rule from TF004 because a post-synth check receives no options
14
+ * (`PostSynthCheck.check(ctx)`), while the preset mechanism already carries
15
+ * exactly this on/off choice per rule id (#3190).
16
+ *
17
+ * A module that has no `version` is TF004's finding and is skipped here.
18
+ *
19
+ * Scope: root and child modules alike (#2112).
20
+ */
21
+
22
+ import type {
23
+ PostSynthCheck,
24
+ PostSynthContext,
25
+ PostSynthDiagnostic,
26
+ } from "@intentius/chant/lint/post-synth";
27
+ import { isResourceDeclarable } from "@intentius/chant/declarable";
28
+ import { MODULE_TYPE, type BlockBody } from "../../hcl/parse";
29
+ import { classifyModuleSource, isExactVersionConstraint } from "./module-source";
30
+
31
+ export const tf039: PostSynthCheck = {
32
+ id: "TF039",
33
+ description: "Registry module version is a range, not an exact version",
34
+
35
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
36
+ const diagnostics: PostSynthDiagnostic[] = [];
37
+
38
+ for (const [key, entity] of ctx.entities) {
39
+ if (entity.entityType !== MODULE_TYPE || !isResourceDeclarable(entity)) continue;
40
+ const props = entity.props as { address?: unknown; body?: unknown };
41
+ const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
42
+ const source = typeof body.source === "string" ? body.source : undefined;
43
+ if (source === undefined || classifyModuleSource(source).kind !== "registry") continue;
44
+
45
+ const version = body.version;
46
+ if (typeof version !== "string" || version.trim() === "") continue;
47
+ if (isExactVersionConstraint(version)) continue;
48
+
49
+ const address = typeof props.address === "string" ? props.address : key;
50
+ diagnostics.push({
51
+ checkId: "TF039",
52
+ severity: "warning",
53
+ message:
54
+ `Module "${address}" sources "${source}" with the version range "${version}". ` +
55
+ "A release inside the range changes what the root provisions with no diff to review. " +
56
+ 'Pin an exact version (`version = "1.4.0"`).',
57
+ entity: key,
58
+ lexicon: "terraform",
59
+ });
60
+ }
61
+
62
+ return diagnostics;
63
+ },
64
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * TF040: a root that uses providers has no `.terraform.lock.hcl`.
3
+ *
4
+ * The lock file records the provider versions and hashes `terraform init`
5
+ * selected. Without it, each `init` resolves `required_providers` constraints
6
+ * afresh, so a provider release changes a plan with no diff. None of
7
+ * TF001 to TF029 looks at it.
8
+ *
9
+ * "Committed" is decided by presence: the file sits beside the root's `.tf`
10
+ * files in the source tree being parsed (`props.lockFile`, stamped by
11
+ * `parseTerraformRootDir` and by the local audit path). Whether git tracks it
12
+ * is not asked, so a lock file that is present but gitignored counts as
13
+ * committed. A parse with no directory to look in (a fetched repository,
14
+ * inline source) leaves `lockFile` unset and the rule reports nothing: not
15
+ * determined, not clean.
16
+ *
17
+ * A root that implies no provider (no `provider` block and no resource or data
18
+ * type) gets no lock file from `init`, so it is skipped, as is a live
19
+ * choudoufu root, which runs no `init`. One diagnostic per root, anchored on
20
+ * the first block that implies a provider.
21
+ *
22
+ * Off by default in both places: the `report-only` tier keeps it out of a
23
+ * build's `recommended` preset, and `auditOptIn` keeps it out of `chant audit`.
24
+ * Many estates run `init` in CI and never commit the file.
25
+ *
26
+ * Scope: root modules only (#2112); a child module has no lock file of its own.
27
+ */
28
+
29
+ import type {
30
+ PostSynthCheck,
31
+ PostSynthContext,
32
+ PostSynthDiagnostic,
33
+ } from "@intentius/chant/lint/post-synth";
34
+ import { isResourceDeclarable } from "@intentius/chant/declarable";
35
+ import { LOCK_FILENAME } from "../../hcl/parse";
36
+ import { isRootScoped } from "./scope";
37
+ import { impliedProviders } from "./tf002";
38
+
39
+ export const tf040: PostSynthCheck = {
40
+ id: "TF040",
41
+ description: "Root has no committed .terraform.lock.hcl",
42
+ // `chant audit` runs every rule, report-only ones included, so without this a
43
+ // new rule would change every project's audit output. A `lint.rules` entry
44
+ // for TF040 turns it on in the audit as well as in a build (#3190).
45
+ auditOptIn: true,
46
+
47
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
48
+ const diagnostics: PostSynthDiagnostic[] = [];
49
+ const missing = new Set<string>();
50
+
51
+ for (const entity of ctx.entities.values()) {
52
+ if (!isResourceDeclarable(entity) || !isRootScoped(entity)) continue;
53
+ const props = entity.props as { root?: unknown; lockFile?: unknown; mode?: unknown };
54
+ if (props.lockFile !== false || props.mode === "live" || typeof props.root !== "string") continue;
55
+ missing.add(props.root);
56
+ }
57
+
58
+ for (const root of [...missing].sort()) {
59
+ const implied = impliedProviders(ctx.entities, root);
60
+ if (implied.size === 0) continue;
61
+ const names = [...implied.keys()].sort();
62
+ diagnostics.push({
63
+ checkId: "TF040",
64
+ severity: "warning",
65
+ message:
66
+ `Root module "${root}" uses ${names.length === 1 ? "provider" : "providers"} ${names.map((n) => `"${n}"`).join(", ")} ` +
67
+ `but has no ${LOCK_FILENAME} beside its .tf files. Run \`terraform init\` and commit the file, ` +
68
+ "so every init selects the same provider versions.",
69
+ entity: implied.get(names[0]),
70
+ lexicon: "terraform",
71
+ });
72
+ }
73
+
74
+ return diagnostics;
75
+ },
76
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * TF041: a Terragrunt `dependency` has `mock_outputs` and lets them stand in
3
+ * for `apply`.
4
+ *
5
+ * `mock_outputs` is what a dependency returns when it has no outputs yet.
6
+ * `mock_outputs_allowed_terraform_commands` limits which commands may use
7
+ * them. Terragrunt reads a missing list and an empty list the same way, as no
8
+ * limit (`shouldReturnMockOutputs` in `pkg/config/dependency.go`, v1.1.6:
9
+ * `nil || len == 0 || slices.Contains(list, command)`). So a dependency with
10
+ * mocks and no list, or a list that names `apply`, can apply the mock values
11
+ * to real state when the upstream unit has not been applied, and the apply
12
+ * reports success. The usual list is `["validate", "plan"]`.
13
+ *
14
+ * A list that is an expression (`local.mock_commands`) is not determined and
15
+ * passes. A dependency with `enabled = false` returns its mocks whatever the
16
+ * list says; that is a different decision and is not reported here.
17
+ */
18
+
19
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
20
+ import { TERRAGRUNT_DEPENDENCY_TYPE } from "../../hcl/parse";
21
+ import { isExpression, terragruntEntities } from "./terragrunt";
22
+
23
+ export const tf041: PostSynthCheck = {
24
+ id: "TF041",
25
+ description: "Terragrunt dependency lets mock_outputs stand in for apply",
26
+
27
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
28
+ const diagnostics: PostSynthDiagnostic[] = [];
29
+ for (const dep of terragruntEntities(ctx.entities, TERRAGRUNT_DEPENDENCY_TYPE)) {
30
+ if (dep.body.mock_outputs === undefined) continue;
31
+ const allowed = dep.body.mock_outputs_allowed_terraform_commands;
32
+ if (isExpression(allowed)) continue;
33
+
34
+ let problem: string | undefined;
35
+ if (allowed === undefined || (Array.isArray(allowed) && allowed.length === 0)) {
36
+ problem =
37
+ "sets no `mock_outputs_allowed_terraform_commands`, and Terragrunt reads a missing or empty list as every command";
38
+ } else if (Array.isArray(allowed) && allowed.includes("apply")) {
39
+ problem = 'lists "apply" in `mock_outputs_allowed_terraform_commands`';
40
+ }
41
+ if (problem === undefined) continue;
42
+
43
+ diagnostics.push({
44
+ checkId: "TF041",
45
+ severity: "warning",
46
+ message:
47
+ `Dependency "${dep.address.replace(/^dependency\./, "")}" in ${dep.file} sets \`mock_outputs\` and ${problem}. ` +
48
+ "An apply that runs before the upstream unit is applied writes the mock values to real state. " +
49
+ 'Set `mock_outputs_allowed_terraform_commands = ["validate", "plan"]`.',
50
+ entity: dep.key,
51
+ lexicon: "terraform",
52
+ });
53
+ }
54
+ return diagnostics;
55
+ },
56
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * TF042: a Terragrunt `dependency` sets `skip_outputs = true` and
3
+ * `mock_outputs`.
4
+ *
5
+ * With `skip_outputs = true` Terragrunt never reads the upstream unit's
6
+ * outputs (`shouldGetOutputs` in `pkg/config/dependency.go`, v1.1.6), so the
7
+ * only values the dependency can return are the mocks. Every plan and apply
8
+ * the mocks are allowed for runs on them, however far the upstream unit has
9
+ * been applied. The pair is sometimes meant, to break a cycle or to avoid
10
+ * reading state, and then the mock values are the configuration; that is
11
+ * worth writing as inputs, which a reader of the unit can see.
12
+ *
13
+ * Only a literal `skip_outputs = true` is reported. A `skip_outputs` built
14
+ * from an expression is not determined.
15
+ */
16
+
17
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
18
+ import { TERRAGRUNT_DEPENDENCY_TYPE } from "../../hcl/parse";
19
+ import { terragruntEntities } from "./terragrunt";
20
+
21
+ export const tf042: PostSynthCheck = {
22
+ id: "TF042",
23
+ description: "Terragrunt dependency sets skip_outputs together with mock_outputs",
24
+
25
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
26
+ const diagnostics: PostSynthDiagnostic[] = [];
27
+ for (const dep of terragruntEntities(ctx.entities, TERRAGRUNT_DEPENDENCY_TYPE)) {
28
+ if (dep.body.skip_outputs !== true || dep.body.mock_outputs === undefined) continue;
29
+ diagnostics.push({
30
+ checkId: "TF042",
31
+ severity: "warning",
32
+ message:
33
+ `Dependency "${dep.address.replace(/^dependency\./, "")}" in ${dep.file} sets \`skip_outputs = true\` with \`mock_outputs\`. ` +
34
+ "Terragrunt then never reads the upstream unit's outputs, so every plan and apply runs on the mock values. " +
35
+ "Drop `skip_outputs`, or pass the values as `inputs` so they are visible as configuration.",
36
+ entity: dep.key,
37
+ lexicon: "terraform",
38
+ });
39
+ }
40
+ return diagnostics;
41
+ },
42
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * TF043: a Terragrunt unit's `terraform.source` names no version.
3
+ *
4
+ * A git source with no `?ref=` follows the default branch, a `tfr://` source
5
+ * with no `?version=` follows the registry's latest release, and an `oci://`
6
+ * source with no tag or digest pulls `latest`. A push or a release changes
7
+ * what the unit provisions with no diff to review. TF005 and TF038 do the same
8
+ * for `module` blocks; a Terragrunt unit has no `module` block, its source is
9
+ * the `source` argument of the `terraform` block.
10
+ *
11
+ * A local path and a source built from an expression (`${local.base}//x`) are
12
+ * not determined and pass. A mutable `?ref=main` is not judged
13
+ * here: the rule asks only whether a version is named.
14
+ */
15
+
16
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
17
+ import { TERRAGRUNT_SOURCE_TYPE } from "../../hcl/parse";
18
+ import { classifyTerragruntSource, terragruntEntities } from "./terragrunt";
19
+
20
+ const FIX: Record<string, string> = {
21
+ git: "Add `?ref=` with a tag or commit SHA.",
22
+ tfr: "Add `?version=` with an exact version.",
23
+ oci: "Add `?digest=sha256:...` or `?tag=` with an exact version tag.",
24
+ };
25
+
26
+ export const tf043: PostSynthCheck = {
27
+ id: "TF043",
28
+ description: "Terragrunt terraform.source names no version (git ref, tfr version or oci tag)",
29
+
30
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
31
+ const diagnostics: PostSynthDiagnostic[] = [];
32
+ for (const block of terragruntEntities(ctx.entities, TERRAGRUNT_SOURCE_TYPE)) {
33
+ const source = block.body.source;
34
+ if (typeof source !== "string") continue;
35
+ const classified = classifyTerragruntSource(source);
36
+ if (classified.pinned !== false) continue;
37
+ diagnostics.push({
38
+ checkId: "TF043",
39
+ severity: "warning",
40
+ message:
41
+ `The \`terraform\` block in ${block.file} sources "${source}" with no version, so a push or a release changes the unit with no diff to review. ` +
42
+ FIX[classified.kind],
43
+ entity: block.key,
44
+ lexicon: "terraform",
45
+ });
46
+ }
47
+ return diagnostics;
48
+ },
49
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * TF044: a Terragrunt config keeps state in a local backend.
3
+ *
4
+ * Read from `remote_state { backend = "local" }` and from a `generate` block
5
+ * whose `contents` declare `backend "local"`. State on the runner's disk is
6
+ * gone when the job ends, a second job cannot read what the first applied, and
7
+ * a git-range run, which checks out each side in a temporary directory, never
8
+ * sees state at all. A shared backend (S3, GCS, azurerm, a Terraform HTTP
9
+ * backend) holds it.
10
+ *
11
+ * A backend named by an expression is not determined and passes. A local
12
+ * backend is sometimes right for a throwaway sandbox; suppress the rule there
13
+ * with `# chant-ignore TF044`.
14
+ */
15
+
16
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
17
+ import { TERRAGRUNT_GENERATE_TYPE, TERRAGRUNT_REMOTE_STATE_TYPE } from "../../hcl/parse";
18
+ import { terragruntEntities } from "./terragrunt";
19
+
20
+ const LOCAL_BACKEND_BLOCK = /\bbackend\s+"local"/;
21
+
22
+ export const tf044: PostSynthCheck = {
23
+ id: "TF044",
24
+ description: "Terragrunt config keeps state in a local backend",
25
+
26
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
27
+ const diagnostics: PostSynthDiagnostic[] = [];
28
+ const report = (key: string, file: string, how: string): void => {
29
+ diagnostics.push({
30
+ checkId: "TF044",
31
+ severity: "warning",
32
+ message:
33
+ `${how} in ${file} keeps state in a local backend. The state lives on the runner's disk, so a later job cannot read it ` +
34
+ "and a git-range run, which checks out each side in a temporary directory, never sees it. Use a shared backend such as S3 or GCS.",
35
+ entity: key,
36
+ lexicon: "terraform",
37
+ });
38
+ };
39
+
40
+ for (const block of terragruntEntities(ctx.entities, TERRAGRUNT_REMOTE_STATE_TYPE)) {
41
+ if (block.body.backend === "local") report(block.key, block.file, "`remote_state` sets `backend = \"local\"`");
42
+ }
43
+ for (const block of terragruntEntities(ctx.entities, TERRAGRUNT_GENERATE_TYPE)) {
44
+ const contents = block.body.contents;
45
+ if (typeof contents === "string" && LOCAL_BACKEND_BLOCK.test(contents)) {
46
+ report(block.key, block.file, `\`generate "${block.address.replace(/^generate\./, "")}"\` writes a \`backend "local"\` block`);
47
+ }
48
+ }
49
+ return diagnostics;
50
+ },
51
+ };