@intentius/chant-lexicon-terraform 0.101.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.
- package/dist/__fixtures__/terragrunt/affected-cases.d.ts +23 -0
- package/dist/__fixtures__/terragrunt/affected-cases.d.ts.map +1 -0
- package/dist/__fixtures__/terragrunt/record.d.ts +17 -0
- package/dist/__fixtures__/terragrunt/record.d.ts.map +1 -0
- package/dist/change-set.d.ts +67 -0
- package/dist/change-set.d.ts.map +1 -0
- package/dist/changed-roots.d.ts +30 -0
- package/dist/changed-roots.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +1 -1
- package/dist/commands.d.ts +26 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/components/capability-plugin.d.ts +54 -0
- package/dist/components/capability-plugin.d.ts.map +1 -0
- package/dist/composites/terraform-pin-rollout-op.d.ts +44 -0
- package/dist/composites/terraform-pin-rollout-op.d.ts.map +1 -0
- package/dist/config.d.ts +3 -3
- package/dist/hcl/descend.d.ts +1 -1
- package/dist/hcl/descend.d.ts.map +1 -1
- package/dist/hcl/edges.d.ts +2 -2
- package/dist/hcl/edges.d.ts.map +1 -1
- package/dist/hcl/jsonencode.d.ts +106 -0
- package/dist/hcl/jsonencode.d.ts.map +1 -0
- package/dist/hcl/marker.d.ts.map +1 -1
- package/dist/hcl/parse.d.ts +29 -1
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/roots.d.ts +1 -1
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/suppressions.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +22 -4
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +1 -1
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/flat-attributes.d.ts +69 -0
- package/dist/lint/post-synth/flat-attributes.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/module-source.d.ts +27 -1
- package/dist/lint/post-synth/module-source.d.ts.map +1 -1
- package/dist/lint/post-synth/terragrunt.d.ts +37 -0
- package/dist/lint/post-synth/terragrunt.d.ts.map +1 -0
- package/dist/lint/post-synth/tf002.d.ts +3 -1
- package/dist/lint/post-synth/tf002.d.ts.map +1 -1
- package/dist/lint/post-synth/tf030.d.ts +51 -0
- package/dist/lint/post-synth/tf030.d.ts.map +1 -0
- package/dist/lint/post-synth/tf031.d.ts +55 -0
- package/dist/lint/post-synth/tf031.d.ts.map +1 -0
- package/dist/lint/post-synth/tf032.d.ts +71 -0
- package/dist/lint/post-synth/tf032.d.ts.map +1 -0
- package/dist/lint/post-synth/tf033.d.ts +50 -0
- package/dist/lint/post-synth/tf033.d.ts.map +1 -0
- package/dist/lint/post-synth/tf034.d.ts +25 -0
- package/dist/lint/post-synth/tf034.d.ts.map +1 -0
- package/dist/lint/post-synth/tf035.d.ts +33 -0
- package/dist/lint/post-synth/tf035.d.ts.map +1 -0
- package/dist/lint/post-synth/tf036.d.ts +33 -0
- package/dist/lint/post-synth/tf036.d.ts.map +1 -0
- package/dist/lint/post-synth/tf037.d.ts +31 -0
- package/dist/lint/post-synth/tf037.d.ts.map +1 -0
- package/dist/lint/post-synth/tf038.d.ts +22 -0
- package/dist/lint/post-synth/tf038.d.ts.map +1 -0
- package/dist/lint/post-synth/tf039.d.ts +23 -0
- package/dist/lint/post-synth/tf039.d.ts.map +1 -0
- package/dist/lint/post-synth/tf040.d.ts +30 -0
- package/dist/lint/post-synth/tf040.d.ts.map +1 -0
- package/dist/lint/post-synth/tf041.d.ts +20 -0
- package/dist/lint/post-synth/tf041.d.ts.map +1 -0
- package/dist/lint/post-synth/tf042.d.ts +18 -0
- package/dist/lint/post-synth/tf042.d.ts.map +1 -0
- package/dist/lint/post-synth/tf043.d.ts +17 -0
- package/dist/lint/post-synth/tf043.d.ts.map +1 -0
- package/dist/lint/post-synth/tf044.d.ts +17 -0
- package/dist/lint/post-synth/tf044.d.ts.map +1 -0
- package/dist/lint/post-synth/tf045.d.ts +22 -0
- package/dist/lint/post-synth/tf045.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +16 -0
- package/dist/okf/rules/TF030.md +11 -0
- package/dist/okf/rules/TF031.md +11 -0
- package/dist/okf/rules/TF032.md +11 -0
- package/dist/okf/rules/TF033.md +11 -0
- package/dist/okf/rules/TF034.md +11 -0
- package/dist/okf/rules/TF035.md +11 -0
- package/dist/okf/rules/TF036.md +11 -0
- package/dist/okf/rules/TF037.md +11 -0
- package/dist/okf/rules/TF038.md +11 -0
- package/dist/okf/rules/TF039.md +11 -0
- package/dist/okf/rules/TF040.md +11 -0
- package/dist/okf/rules/TF041.md +11 -0
- package/dist/okf/rules/TF042.md +11 -0
- package/dist/okf/rules/TF043.md +11 -0
- package/dist/okf/rules/TF044.md +11 -0
- package/dist/okf/rules/TF045.md +11 -0
- package/dist/op/activities/index.d.ts +4 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/pin-rollout.d.ts +46 -0
- package/dist/op/activities/pin-rollout.d.ts.map +1 -0
- package/dist/op/activities/terraform.d.ts +24 -50
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +66 -18
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/pin/edit-ts.d.ts +21 -0
- package/dist/pin/edit-ts.d.ts.map +1 -0
- package/dist/pin/edit.d.ts +52 -0
- package/dist/pin/edit.d.ts.map +1 -0
- package/dist/pin/forge.d.ts +39 -0
- package/dist/pin/forge.d.ts.map +1 -0
- package/dist/pin/index.d.ts +22 -0
- package/dist/pin/index.d.ts.map +1 -0
- package/dist/pin/rollout.d.ts +116 -0
- package/dist/pin/rollout.d.ts.map +1 -0
- package/dist/pin/scan.d.ts +54 -0
- package/dist/pin/scan.d.ts.map +1 -0
- package/dist/pin/source.d.ts +93 -0
- package/dist/pin/source.d.ts.map +1 -0
- package/dist/pin/waves.d.ts +66 -0
- package/dist/pin/waves.d.ts.map +1 -0
- package/dist/plan-digest.d.ts +59 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/flat-attributes.ts +146 -0
- package/dist/rules/module-source.ts +56 -1
- package/dist/rules/terragrunt.ts +76 -0
- package/dist/rules/tf002.ts +1 -1
- package/dist/rules/tf030.ts +290 -0
- package/dist/rules/tf031.ts +370 -0
- package/dist/rules/tf032.ts +226 -0
- package/dist/rules/tf033.ts +160 -0
- package/dist/rules/tf034.ts +63 -0
- package/dist/rules/tf035.ts +74 -0
- package/dist/rules/tf036.ts +119 -0
- package/dist/rules/tf037.ts +101 -0
- package/dist/rules/tf038.ts +71 -0
- package/dist/rules/tf039.ts +64 -0
- package/dist/rules/tf040.ts +76 -0
- package/dist/rules/tf041.ts +56 -0
- package/dist/rules/tf042.ts +42 -0
- package/dist/rules/tf043.ts +49 -0
- package/dist/rules/tf044.ts +51 -0
- package/dist/rules/tf045.ts +47 -0
- package/dist/terragrunt/affected.d.ts +119 -0
- package/dist/terragrunt/affected.d.ts.map +1 -0
- package/dist/terragrunt/index.d.ts +11 -0
- package/dist/terragrunt/index.d.ts.map +1 -0
- package/dist/terragrunt/mocks.d.ts +147 -0
- package/dist/terragrunt/mocks.d.ts.map +1 -0
- package/dist/terragrunt/run.d.ts +130 -0
- package/dist/terragrunt/run.d.ts.map +1 -0
- package/dist/terragrunt/units.d.ts +103 -0
- package/dist/terragrunt/units.d.ts.map +1 -0
- package/dist/terragrunt/wave.d.ts +89 -0
- package/dist/terragrunt/wave.d.ts.map +1 -0
- package/package.json +13 -3
- package/src/__fixtures__/change-set/choudoufu-set-plan.change-set.golden.json +95 -0
- package/src/__fixtures__/change-set/choudoufu-set-plan.json +76 -0
- package/src/__fixtures__/change-set/choudoufu.part.golden.json +173 -0
- package/src/__fixtures__/change-set/choudoufu.plan.json +610 -0
- package/src/__fixtures__/change-set/terraform.import-forget.plan.json +81 -0
- package/src/__fixtures__/change-set/terraform.part.golden.json +173 -0
- package/src/__fixtures__/change-set/terraform.plan.json +589 -0
- package/src/__fixtures__/change-set/tofu.part.golden.json +173 -0
- package/src/__fixtures__/change-set/tofu.plan.json +588 -0
- package/src/__fixtures__/change-set/v1.tf.txt +21 -0
- package/src/__fixtures__/change-set/v2-module-app.tf.txt +4 -0
- package/src/__fixtures__/change-set/v2.tf.txt +21 -0
- package/src/__fixtures__/pin/choudoufu-live-waves-n5.json +83 -0
- package/src/__fixtures__/pin/largeset-oci-n5/apply-order.txt +5 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/main.tf +33 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/main.tf +45 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/main.tf +47 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/main.tf +31 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/main.tf +27 -0
- package/src/__fixtures__/pin/largeset-oci-n5/fixture.json +77 -0
- package/src/__fixtures__/plan-summary/largeset-n5-bump.json +2071 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.json +95 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.md +22 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.txt +12 -0
- package/src/__fixtures__/terragrunt/affected/catalog/units/web/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/gitignore +3 -0
- package/src/__fixtures__/terragrunt/affected/live/dev/app/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/dev/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/prod/app/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/stg/terragrunt.stack.hcl +5 -0
- package/src/__fixtures__/terragrunt/affected/modules/app/main.tf +6 -0
- package/src/__fixtures__/terragrunt/affected/modules/app/policy.json +1 -0
- package/src/__fixtures__/terragrunt/affected/modules/net/main.tf +2 -0
- package/src/__fixtures__/terragrunt/affected/modules/thing/main.tf +3 -0
- package/src/__fixtures__/terragrunt/affected/root.hcl +5 -0
- package/src/__fixtures__/terragrunt/affected-cases.ts +126 -0
- package/src/__fixtures__/terragrunt/five-units/catalog/units/thing/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/app/terragrunt.hcl +13 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/db/terragrunt.hcl +8 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/live/prod/app/terragrunt.hcl +8 -0
- package/src/__fixtures__/terragrunt/five-units/live/prod/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/modules/thing/main.tf +7 -0
- package/src/__fixtures__/terragrunt/five-units/root.hcl +5 -0
- package/src/__fixtures__/terragrunt/record.ts +56 -0
- package/src/__fixtures__/terragrunt/recorded/affected-find.json +50 -0
- package/src/__fixtures__/terragrunt/recorded/failed-plan-report.json +36 -0
- package/src/__fixtures__/terragrunt/recorded/find.json +67 -0
- package/src/__fixtures__/terragrunt/recorded/version.txt +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/apply-report.json +22 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/dev/vpc/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/prod/vpc/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/plan-report.json +22 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/dev/db/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/prod/app/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/plan-report.json +22 -0
- package/src/change-set-combined-run.test.ts +156 -0
- package/src/change-set.test.ts +151 -0
- package/src/change-set.ts +273 -0
- package/src/changed-roots.test.ts +58 -0
- package/src/changed-roots.ts +102 -0
- package/src/commands.ts +112 -0
- package/src/components/capability-plugin.test.ts +91 -0
- package/src/components/capability-plugin.ts +131 -0
- package/src/components/pr-loop.tofu.test.ts +244 -0
- package/src/composites/terraform-pin-rollout-op.test.ts +69 -0
- package/src/composites/terraform-pin-rollout-op.ts +60 -0
- package/src/hcl/descend.ts +1 -1
- package/src/hcl/edges.ts +2 -2
- package/src/hcl/jsonencode.test.ts +226 -0
- package/src/hcl/jsonencode.ts +454 -0
- package/src/hcl/parse.test.ts +25 -0
- package/src/hcl/parse.ts +91 -6
- package/src/hcl/roots.ts +1 -1
- package/src/hcl/suppressions.ts +8 -2
- package/src/index.ts +18 -0
- package/src/lint/audit-catalog.ts +230 -0
- package/src/lint/audit-lineage.ts +155 -1
- package/src/lint/audit.test.ts +3 -1
- package/src/lint/post-synth/estate-2284.test.ts +68 -0
- package/src/lint/post-synth/fixtures/TF030/negative-rules.tf +45 -0
- package/src/lint/post-synth/fixtures/TF030/negative.tf +55 -0
- package/src/lint/post-synth/fixtures/TF030/not-determined-port.tf +28 -0
- package/src/lint/post-synth/fixtures/TF030/not-determined-standalone.tf +32 -0
- package/src/lint/post-synth/fixtures/TF030/not-determined-vpc-rule.tf +21 -0
- package/src/lint/post-synth/fixtures/TF030/not-determined.tf +39 -0
- package/src/lint/post-synth/fixtures/TF030/positive-ipv6.tf +44 -0
- package/src/lint/post-synth/fixtures/TF030/positive-mixed.tf +36 -0
- package/src/lint/post-synth/fixtures/TF030/positive-standalone.tf +25 -0
- package/src/lint/post-synth/fixtures/TF030/positive.tf +43 -0
- package/src/lint/post-synth/fixtures/TF031/negative.tf +89 -0
- package/src/lint/post-synth/fixtures/TF031/not-determined.tf +139 -0
- package/src/lint/post-synth/fixtures/TF031/positive-data-source.tf +57 -0
- package/src/lint/post-synth/fixtures/TF031/positive-heredoc.tf +149 -0
- package/src/lint/post-synth/fixtures/TF031/positive-jsonencode.tf +115 -0
- package/src/lint/post-synth/fixtures/TF032/negative-heredoc.tf +20 -0
- package/src/lint/post-synth/fixtures/TF032/negative-references.tf +214 -0
- package/src/lint/post-synth/fixtures/TF032/negative.tf +99 -0
- package/src/lint/post-synth/fixtures/TF032/positive-heredoc.tf +22 -0
- package/src/lint/post-synth/fixtures/TF032/positive-sidecar.tf +197 -0
- package/src/lint/post-synth/fixtures/TF032/positive.tf +54 -0
- package/src/lint/post-synth/fixtures/TF033/negative.tf +39 -0
- package/src/lint/post-synth/fixtures/TF033/not-determined-aurora.tf +25 -0
- package/src/lint/post-synth/fixtures/TF033/positive-absent.tf +93 -0
- package/src/lint/post-synth/fixtures/TF033/positive.tf +50 -0
- package/src/lint/post-synth/fixtures/TF034/negative.tf +24 -0
- package/src/lint/post-synth/fixtures/TF034/positive-absent.tf +36 -0
- package/src/lint/post-synth/fixtures/TF034/positive.tf +14 -0
- package/src/lint/post-synth/fixtures/TF035/negative-absent.tf +43 -0
- package/src/lint/post-synth/fixtures/TF035/negative.tf +54 -0
- package/src/lint/post-synth/fixtures/TF035/positive.tf +37 -0
- package/src/lint/post-synth/fixtures/TF036/negative.tf +61 -0
- package/src/lint/post-synth/fixtures/TF036/not-determined.tf +28 -0
- package/src/lint/post-synth/fixtures/TF036/positive.tf +38 -0
- package/src/lint/post-synth/fixtures/TF037/negative-exclusion.tf +63 -0
- package/src/lint/post-synth/fixtures/TF037/negative.tf +27 -0
- package/src/lint/post-synth/fixtures/TF037/not-determined.tf +27 -0
- package/src/lint/post-synth/fixtures/TF037/positive-absent.tf +15 -0
- package/src/lint/post-synth/fixtures/TF037/positive.tf +26 -0
- package/src/lint/post-synth/fixtures/TF038/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF039/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF041/negative/terragrunt.hcl +12 -0
- package/src/lint/post-synth/fixtures/TF041/positive/terragrunt.hcl +7 -0
- package/src/lint/post-synth/fixtures/TF041/positive-apply/terragrunt.hcl +8 -0
- package/src/lint/post-synth/fixtures/TF042/negative/terragrunt.hcl +13 -0
- package/src/lint/post-synth/fixtures/TF042/positive/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF043/negative/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/negative-other/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-git/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-oci/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-tfr/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF044/negative/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF044/positive/terragrunt.hcl +7 -0
- package/src/lint/post-synth/fixtures/TF044/positive-generate/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF045/negative/root.hcl +1 -0
- package/src/lint/post-synth/fixtures/TF045/negative/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF045/positive/root.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF045/positive/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF045/unit-only/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/estate-2284/clean/main.tf +132 -0
- package/src/lint/post-synth/fixtures/estate-2284/violating/main.tf +112 -0
- package/src/lint/post-synth/flat-attributes.ts +146 -0
- package/src/lint/post-synth/index.ts +32 -0
- package/src/lint/post-synth/module-source.test.ts +42 -1
- package/src/lint/post-synth/module-source.ts +56 -1
- package/src/lint/post-synth/terragrunt.ts +76 -0
- package/src/lint/post-synth/tf002.ts +1 -1
- package/src/lint/post-synth/tf030.test.ts +219 -0
- package/src/lint/post-synth/tf030.ts +290 -0
- package/src/lint/post-synth/tf031.test.ts +229 -0
- package/src/lint/post-synth/tf031.ts +370 -0
- package/src/lint/post-synth/tf032.test.ts +127 -0
- package/src/lint/post-synth/tf032.ts +226 -0
- package/src/lint/post-synth/tf033.test.ts +126 -0
- package/src/lint/post-synth/tf033.ts +160 -0
- package/src/lint/post-synth/tf034.test.ts +71 -0
- package/src/lint/post-synth/tf034.ts +63 -0
- package/src/lint/post-synth/tf035.test.ts +68 -0
- package/src/lint/post-synth/tf035.ts +74 -0
- package/src/lint/post-synth/tf036.test.ts +87 -0
- package/src/lint/post-synth/tf036.ts +119 -0
- package/src/lint/post-synth/tf037.test.ts +88 -0
- package/src/lint/post-synth/tf037.ts +101 -0
- package/src/lint/post-synth/tf038.test.ts +51 -0
- package/src/lint/post-synth/tf038.ts +71 -0
- package/src/lint/post-synth/tf039.test.ts +40 -0
- package/src/lint/post-synth/tf039.ts +64 -0
- package/src/lint/post-synth/tf040.test.ts +55 -0
- package/src/lint/post-synth/tf040.ts +76 -0
- package/src/lint/post-synth/tf041.test.ts +50 -0
- package/src/lint/post-synth/tf041.ts +56 -0
- package/src/lint/post-synth/tf042.test.ts +19 -0
- package/src/lint/post-synth/tf042.ts +42 -0
- package/src/lint/post-synth/tf043.test.ts +65 -0
- package/src/lint/post-synth/tf043.ts +49 -0
- package/src/lint/post-synth/tf044.test.ts +27 -0
- package/src/lint/post-synth/tf044.ts +51 -0
- package/src/lint/post-synth/tf045.test.ts +30 -0
- package/src/lint/post-synth/tf045.ts +47 -0
- package/src/op/activities/index.ts +4 -0
- package/src/op/activities/pin-rollout.ts +77 -0
- package/src/op/activities/terraform.test.ts +13 -0
- package/src/op/activities/terraform.ts +46 -86
- package/src/op/activity-contracts.test.ts +4 -1
- package/src/op/activity-contracts.ts +42 -0
- package/src/op/builders.ts +13 -0
- package/src/pin/bundle.test.ts +53 -0
- package/src/pin/edit-ts.test.ts +39 -0
- package/src/pin/edit-ts.ts +77 -0
- package/src/pin/edit.test.ts +199 -0
- package/src/pin/edit.ts +157 -0
- package/src/pin/forge.ts +85 -0
- package/src/pin/index.ts +22 -0
- package/src/pin/rollout.test.ts +232 -0
- package/src/pin/rollout.ts +451 -0
- package/src/pin/scan.ts +309 -0
- package/src/pin/source.ts +206 -0
- package/src/pin/waves.test.ts +101 -0
- package/src/pin/waves.ts +125 -0
- package/src/plan-digest.ts +99 -0
- package/src/plan-summary.test.ts +154 -0
- package/src/plugin.ts +25 -2
- package/src/terragrunt/affected.acceptance.test.ts +83 -0
- package/src/terragrunt/affected.test.ts +177 -0
- package/src/terragrunt/affected.ts +348 -0
- package/src/terragrunt/index.ts +11 -0
- package/src/terragrunt/mocks.acceptance.test.ts +105 -0
- package/src/terragrunt/mocks.test.ts +309 -0
- package/src/terragrunt/mocks.ts +324 -0
- package/src/terragrunt/run.ts +369 -0
- package/src/terragrunt/terragrunt.acceptance.test.ts +122 -0
- package/src/terragrunt/terragrunt.test.ts +332 -0
- package/src/terragrunt/units.ts +235 -0
- package/src/terragrunt/wave.ts +181 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the argument of a `jsonencode(...)` call as structure, when it is
|
|
3
|
+
* literal (chant #2286, shared with #2287).
|
|
4
|
+
*
|
|
5
|
+
* hcl2json does not evaluate expressions. `policy = jsonencode({...})` and
|
|
6
|
+
* `container_definitions = jsonencode([...])` both reach a block body as ONE
|
|
7
|
+
* string holding the expression's source text, wrapped as an interpolation:
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* "${jsonencode({\n Version = \"2012-10-17\"\n Statement = [{ Action = \"*\" }]\n })}"
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* The text inside is HCL object syntax, not JSON (`=` or `:` separators, bare
|
|
14
|
+
* keys, newline-separated items, `#` comments). A rule that wants the policy
|
|
15
|
+
* or the container definitions has to read that text itself. This file does
|
|
16
|
+
* so, narrowly: it accepts literal objects, tuples, quoted strings, numbers,
|
|
17
|
+
* `true`, `false` and `null`, and nothing else. Any expression in the
|
|
18
|
+
* argument (a reference, a function call, a for-expression, a splat, a
|
|
19
|
+
* conditional, an operator, a template interpolation inside a string, a
|
|
20
|
+
* heredoc, a parenthesised key) makes the whole read `not-determined`, with a
|
|
21
|
+
* reason naming what was found. It never evaluates anything, so it can never
|
|
22
|
+
* return a value Terraform would not also produce from the same text.
|
|
23
|
+
*
|
|
24
|
+
* Three answers:
|
|
25
|
+
* - `literal`: the value is a single whole-string `${jsonencode(...)}` and
|
|
26
|
+
* its argument is fully literal. `value` is the plain JS value
|
|
27
|
+
* `jsonencode` would encode.
|
|
28
|
+
* - `not-determined`: the value is a `${jsonencode(...)}` call, but the
|
|
29
|
+
* argument holds something this reader does not evaluate.
|
|
30
|
+
* - `not-jsonencode`: the value is not a `${jsonencode(...)}` call at all
|
|
31
|
+
* (a plain string, a reference, `file(...)`, a number, absent). Callers
|
|
32
|
+
* decide what that means for their attribute; a plain string, for example,
|
|
33
|
+
* may itself be JSON from a heredoc.
|
|
34
|
+
*
|
|
35
|
+
* The whole-string test is shape-based: the string starts `${jsonencode(` and
|
|
36
|
+
* ends `)}`. A string that matches that shape but is really a template with
|
|
37
|
+
* two interpolations (`${jsonencode(a)}-${jsonencode(b)}`) fails the literal
|
|
38
|
+
* parse at the point where the first call closes early, and reads as
|
|
39
|
+
* `not-determined`, never as `literal`.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export type JsonencodeRead =
|
|
43
|
+
/** The argument is a fully literal HCL object/tuple/string/number/bool/null tree. */
|
|
44
|
+
| { kind: "literal"; value: unknown }
|
|
45
|
+
/** `jsonencode(...)` whose argument contains a reference, function call, for-expression, splat, conditional, etc. */
|
|
46
|
+
| { kind: "not-determined"; reason: string }
|
|
47
|
+
/** The value is not a single whole-string `${jsonencode(...)}` at all. */
|
|
48
|
+
| { kind: "not-jsonencode" };
|
|
49
|
+
|
|
50
|
+
export interface ReadJsonencodeOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Read past a non-literal VALUE instead of giving up on the whole argument.
|
|
53
|
+
* Each such value (a reference, a function call, a conditional, an
|
|
54
|
+
* interpolated string, a heredoc, any other expression) becomes an
|
|
55
|
+
* {@link UnknownLeaf} in the returned tree, so a rule can still read the
|
|
56
|
+
* literal fields beside it: `Action = "*"` next to
|
|
57
|
+
* `Resource = aws_s3_bucket.x.arn`, or a container's literal `environment`
|
|
58
|
+
* next to `image = var.image`.
|
|
59
|
+
*
|
|
60
|
+
* The skip is narrow. It consumes one value expression, balancing `()[]{}`
|
|
61
|
+
* and quoted strings, up to the next `,`, newline or closing bracket at its
|
|
62
|
+
* own depth. A member whose KEY is an expression (an interpolated quoted
|
|
63
|
+
* key or a parenthesised computed key) is dropped from its object and listed
|
|
64
|
+
* under {@link UNKNOWN_KEYS} on that object. Damage to the STRUCTURE is
|
|
65
|
+
* still `not-determined`: an expression as the whole argument, including
|
|
66
|
+
* a for-expression or a splat (`jsonencode(local.policy)`,
|
|
67
|
+
* `jsonencode(var.x[*])`), since then no key or element is known.
|
|
68
|
+
*/
|
|
69
|
+
unknownLeaves?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Marks a value position `readJsonencode(raw, { unknownLeaves: true })` could not read. */
|
|
73
|
+
export const UNKNOWN: unique symbol = Symbol("jsonencode.unknown");
|
|
74
|
+
|
|
75
|
+
/** A value position holding an expression; `reason` names what was found there. */
|
|
76
|
+
export interface UnknownLeaf {
|
|
77
|
+
readonly [UNKNOWN]: true;
|
|
78
|
+
readonly reason: string;
|
|
79
|
+
/**
|
|
80
|
+
* The skipped expression's own source text, trimmed (`aws_s3_bucket.x.arn`,
|
|
81
|
+
* `"arn:aws:s3:::${var.bucket}/*"` with its quotes). A rule may read it to
|
|
82
|
+
* rule a value OUT (a quoted template whose literal text already differs
|
|
83
|
+
* from the value it looks for), never to evaluate it.
|
|
84
|
+
*/
|
|
85
|
+
readonly source: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Marks an object, read with `unknownLeaves`, that had members whose KEY is an
|
|
90
|
+
* expression (`"router-${var.n}.rule" = ...`, `(var.k) = ...`). Those members
|
|
91
|
+
* are dropped from the object and their reasons listed here, so a reader can
|
|
92
|
+
* tell "this key is absent" from "some key here could not be read".
|
|
93
|
+
*/
|
|
94
|
+
export const UNKNOWN_KEYS: unique symbol = Symbol("jsonencode.unknownKeys");
|
|
95
|
+
|
|
96
|
+
/** Whether `obj` is an object that dropped one or more members with an expression key. */
|
|
97
|
+
export function hasUnknownKeys(obj: unknown): obj is Record<string, unknown> & { [UNKNOWN_KEYS]: string[] } {
|
|
98
|
+
return (
|
|
99
|
+
typeof obj === "object" &&
|
|
100
|
+
obj !== null &&
|
|
101
|
+
Array.isArray((obj as Record<symbol, unknown>)[UNKNOWN_KEYS]) &&
|
|
102
|
+
((obj as Record<symbol, unknown>)[UNKNOWN_KEYS] as unknown[]).length > 0
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Whether `v` is a value position the reader skipped. */
|
|
107
|
+
export function isUnknown(v: unknown): v is UnknownLeaf {
|
|
108
|
+
return typeof v === "object" && v !== null && (v as Record<symbol, unknown>)[UNKNOWN] === true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const PREFIX = /^\$\{\s*jsonencode\s*\(/;
|
|
112
|
+
const SUFFIX = /\)\s*\}$/;
|
|
113
|
+
|
|
114
|
+
/** Read `raw` (an hcl2json attribute value) as a `jsonencode(...)` call. */
|
|
115
|
+
export function readJsonencode(raw: unknown, options: ReadJsonencodeOptions = {}): JsonencodeRead {
|
|
116
|
+
if (typeof raw !== "string") return { kind: "not-jsonencode" };
|
|
117
|
+
const prefix = PREFIX.exec(raw);
|
|
118
|
+
if (!prefix || !SUFFIX.test(raw)) return { kind: "not-jsonencode" };
|
|
119
|
+
|
|
120
|
+
const reader = new Reader(raw, prefix[0].length, options.unknownLeaves === true);
|
|
121
|
+
try {
|
|
122
|
+
const value = reader.value(0);
|
|
123
|
+
reader.skipSpace();
|
|
124
|
+
// The argument must end at the call's own `)`, then the interpolation's `}`.
|
|
125
|
+
reader.expect(")", "a second argument or an expression after the literal");
|
|
126
|
+
reader.skipSpace();
|
|
127
|
+
reader.expect("}", "text after the jsonencode call");
|
|
128
|
+
if (!reader.atEnd()) throw new NotLiteral("text after the jsonencode call");
|
|
129
|
+
return { kind: "literal", value };
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (err instanceof NotLiteral) return { kind: "not-determined", reason: err.message };
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
class NotLiteral extends Error {}
|
|
137
|
+
|
|
138
|
+
/** Damage to the shape itself: never recovered by skipping a leaf. */
|
|
139
|
+
class Structural extends NotLiteral {}
|
|
140
|
+
|
|
141
|
+
/** An object key that is an expression; recoverable by dropping the member under `unknownLeaves`. */
|
|
142
|
+
class KeyNotLiteral extends Structural {}
|
|
143
|
+
|
|
144
|
+
const IDENT = /[A-Za-z_][A-Za-z0-9_-]*/y;
|
|
145
|
+
const NUMBER = /-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
|
|
146
|
+
|
|
147
|
+
/** A cursor over the source text of the jsonencode argument. */
|
|
148
|
+
class Reader {
|
|
149
|
+
constructor(
|
|
150
|
+
private readonly src: string,
|
|
151
|
+
private pos: number,
|
|
152
|
+
private readonly unknownLeaves = false,
|
|
153
|
+
) {}
|
|
154
|
+
|
|
155
|
+
atEnd(): boolean {
|
|
156
|
+
return this.pos >= this.src.length;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private peek(): string {
|
|
160
|
+
return this.src[this.pos] ?? "";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
expect(ch: string, what: string): void {
|
|
164
|
+
if (this.peek() !== ch) throw new NotLiteral(what === "" ? `expected "${ch}"` : what);
|
|
165
|
+
this.pos++;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Whitespace, newlines and the three HCL comment forms. */
|
|
169
|
+
skipSpace(newlines = true): void {
|
|
170
|
+
for (;;) {
|
|
171
|
+
const c = this.peek();
|
|
172
|
+
if (c === " " || c === "\t" || c === "\r" || (newlines && c === "\n")) {
|
|
173
|
+
this.pos++;
|
|
174
|
+
} else if (c === "#" || (c === "/" && this.src[this.pos + 1] === "/")) {
|
|
175
|
+
const end = this.src.indexOf("\n", this.pos);
|
|
176
|
+
this.pos = end === -1 ? this.src.length : end;
|
|
177
|
+
} else if (c === "/" && this.src[this.pos + 1] === "*") {
|
|
178
|
+
const end = this.src.indexOf("*/", this.pos + 2);
|
|
179
|
+
if (end === -1) throw new NotLiteral("an unterminated comment");
|
|
180
|
+
this.pos = end + 2;
|
|
181
|
+
} else {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** One value. `depth` is 0 for the whole argument, which is never skipped. */
|
|
188
|
+
value(depth: number): unknown {
|
|
189
|
+
this.skipSpace();
|
|
190
|
+
if (!this.unknownLeaves || depth === 0) return this.literal(depth);
|
|
191
|
+
const start = this.pos;
|
|
192
|
+
try {
|
|
193
|
+
return this.literal(depth);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (!(err instanceof NotLiteral) || err instanceof Structural) throw err;
|
|
196
|
+
this.pos = start;
|
|
197
|
+
this.skipExpression();
|
|
198
|
+
return { [UNKNOWN]: true, reason: err.message, source: this.src.slice(start, this.pos).trim() } satisfies UnknownLeaf;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private literal(depth: number): unknown {
|
|
203
|
+
const c = this.peek();
|
|
204
|
+
let v: unknown;
|
|
205
|
+
if (c === "{") v = this.object(depth + 1);
|
|
206
|
+
else if (c === "[") v = this.tuple(depth + 1);
|
|
207
|
+
else if (c === '"') v = this.string();
|
|
208
|
+
else if (c === "<" && this.src.startsWith("<<", this.pos)) throw new NotLiteral("a heredoc");
|
|
209
|
+
else if (c === "(") throw new NotLiteral("a parenthesised expression");
|
|
210
|
+
else if (c === "-" || (c >= "0" && c <= "9")) v = this.number();
|
|
211
|
+
else v = this.keyword();
|
|
212
|
+
this.afterValue();
|
|
213
|
+
return v;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* A literal is complete only if what follows it closes or separates. An
|
|
218
|
+
* operator, a conditional, an attribute access or an index after it makes
|
|
219
|
+
* the whole thing an expression.
|
|
220
|
+
*/
|
|
221
|
+
private afterValue(): void {
|
|
222
|
+
this.skipSpace(false);
|
|
223
|
+
const c = this.peek();
|
|
224
|
+
if (c === "" || c === "\n" || c === "," || c === "}" || c === "]" || c === ")") return;
|
|
225
|
+
const next = this.src[this.pos + 1];
|
|
226
|
+
if (c === "#" || (c === "/" && (next === "/" || next === "*"))) return; // a comment, consumed by the caller's skipSpace
|
|
227
|
+
if (c === "?") throw new NotLiteral("a conditional expression");
|
|
228
|
+
if (c === "." || c === "[") throw new NotLiteral("an attribute access, index or splat");
|
|
229
|
+
throw new NotLiteral(`an operator or expression ("${c}")`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private object(depth: number): Record<string, unknown> {
|
|
233
|
+
this.expect("{", "");
|
|
234
|
+
const out: Record<string, unknown> = {};
|
|
235
|
+
for (;;) {
|
|
236
|
+
this.skipSpace();
|
|
237
|
+
if (this.peek() === "}") {
|
|
238
|
+
this.pos++;
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
const quoted = this.peek() === '"';
|
|
242
|
+
const memberStart = this.pos;
|
|
243
|
+
let key: string;
|
|
244
|
+
try {
|
|
245
|
+
key = this.key();
|
|
246
|
+
} catch (err) {
|
|
247
|
+
if (!(err instanceof KeyNotLiteral) || !this.unknownLeaves) throw err;
|
|
248
|
+
// Drop the whole member: the skip runs from the key through its value
|
|
249
|
+
// to the separator, balancing brackets and quotes on the way.
|
|
250
|
+
this.pos = memberStart;
|
|
251
|
+
this.skipExpression();
|
|
252
|
+
const bag = out as Record<symbol, string[]>;
|
|
253
|
+
(bag[UNKNOWN_KEYS] ??= []).push(err.message);
|
|
254
|
+
this.skipSpace(false);
|
|
255
|
+
if (this.peek() === ",") this.pos++;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
this.skipSpace(false);
|
|
259
|
+
const sep = this.peek();
|
|
260
|
+
if (sep !== "=" && sep !== ":") {
|
|
261
|
+
// A for-expression is a VALUE: under unknownLeaves the enclosing
|
|
262
|
+
// value() skips it as a leaf; as the whole argument it is not determined.
|
|
263
|
+
if (!quoted && key === "for") throw new NotLiteral("a for-expression");
|
|
264
|
+
throw new Structural(`an expression where "${key}" needs = or :`);
|
|
265
|
+
}
|
|
266
|
+
this.pos++;
|
|
267
|
+
out[key] = this.value(depth);
|
|
268
|
+
this.skipSpace(false);
|
|
269
|
+
if (this.peek() === ",") this.pos++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private key(): string {
|
|
274
|
+
const c = this.peek();
|
|
275
|
+
if (c === '"') {
|
|
276
|
+
try {
|
|
277
|
+
return this.string();
|
|
278
|
+
} catch (err) {
|
|
279
|
+
if (err instanceof NotLiteral) throw new KeyNotLiteral(`an object key holding ${err.message.replace(/^a string with /, "")}`);
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (c === "(") throw new KeyNotLiteral("a computed object key");
|
|
284
|
+
IDENT.lastIndex = this.pos;
|
|
285
|
+
const m = IDENT.exec(this.src);
|
|
286
|
+
if (!m) throw new Structural(`an unexpected "${c}" where an object key belongs`);
|
|
287
|
+
this.pos += m[0].length;
|
|
288
|
+
return m[0];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private tuple(depth: number): unknown[] {
|
|
292
|
+
this.expect("[", "");
|
|
293
|
+
this.skipSpace();
|
|
294
|
+
if (/^for\s/.test(this.src.slice(this.pos, this.pos + 4))) throw new NotLiteral("a for-expression");
|
|
295
|
+
const out: unknown[] = [];
|
|
296
|
+
for (;;) {
|
|
297
|
+
this.skipSpace();
|
|
298
|
+
if (this.peek() === "]") {
|
|
299
|
+
this.pos++;
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
out.push(this.value(depth));
|
|
303
|
+
this.skipSpace();
|
|
304
|
+
if (this.peek() === ",") this.pos++;
|
|
305
|
+
else if (this.peek() !== "]") throw new NotLiteral("a tuple item followed by an expression");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** A quoted HCL string. A `${` or `%{` (not escaped as `$${`/`%%{`) is a template. */
|
|
310
|
+
private string(): string {
|
|
311
|
+
this.expect('"', "");
|
|
312
|
+
let out = "";
|
|
313
|
+
for (;;) {
|
|
314
|
+
const c = this.peek();
|
|
315
|
+
if (c === "") throw new NotLiteral("an unterminated string");
|
|
316
|
+
if (c === "\n") throw new NotLiteral("a newline inside a quoted string");
|
|
317
|
+
if (c === '"') {
|
|
318
|
+
this.pos++;
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
if ((c === "$" || c === "%") && this.src[this.pos + 1] === c && this.src[this.pos + 2] === "{") {
|
|
322
|
+
out += `${c}{`;
|
|
323
|
+
this.pos += 3;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if ((c === "$" || c === "%") && this.src[this.pos + 1] === "{") {
|
|
327
|
+
throw new NotLiteral(c === "$" ? "a string with a ${...} interpolation" : "a string with a %{...} directive");
|
|
328
|
+
}
|
|
329
|
+
if (c === "\\") {
|
|
330
|
+
out += this.escape();
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
out += c;
|
|
334
|
+
this.pos++;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private escape(): string {
|
|
339
|
+
const e = this.src[this.pos + 1] ?? "";
|
|
340
|
+
this.pos += 2;
|
|
341
|
+
switch (e) {
|
|
342
|
+
case "n":
|
|
343
|
+
return "\n";
|
|
344
|
+
case "r":
|
|
345
|
+
return "\r";
|
|
346
|
+
case "t":
|
|
347
|
+
return "\t";
|
|
348
|
+
case '"':
|
|
349
|
+
return '"';
|
|
350
|
+
case "\\":
|
|
351
|
+
return "\\";
|
|
352
|
+
case "u":
|
|
353
|
+
case "U": {
|
|
354
|
+
const len = e === "u" ? 4 : 8;
|
|
355
|
+
const hex = this.src.slice(this.pos, this.pos + len);
|
|
356
|
+
if (!/^[0-9A-Fa-f]+$/.test(hex) || hex.length !== len) throw new NotLiteral("a malformed unicode escape");
|
|
357
|
+
this.pos += len;
|
|
358
|
+
return String.fromCodePoint(parseInt(hex, 16));
|
|
359
|
+
}
|
|
360
|
+
default:
|
|
361
|
+
throw new NotLiteral(`an unknown string escape "\\${e}"`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private number(): number {
|
|
366
|
+
NUMBER.lastIndex = this.pos;
|
|
367
|
+
const m = NUMBER.exec(this.src);
|
|
368
|
+
if (!m) throw new NotLiteral("a unary operator on an expression");
|
|
369
|
+
this.pos += m[0].length;
|
|
370
|
+
return Number(m[0]);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private keyword(): unknown {
|
|
374
|
+
IDENT.lastIndex = this.pos;
|
|
375
|
+
const m = IDENT.exec(this.src);
|
|
376
|
+
if (!m) throw new NotLiteral(`an unexpected "${this.peek()}"`);
|
|
377
|
+
const word = m[0];
|
|
378
|
+
this.pos += word.length;
|
|
379
|
+
if (word === "true") return true;
|
|
380
|
+
if (word === "false") return false;
|
|
381
|
+
if (word === "null") return null;
|
|
382
|
+
this.skipSpace(false);
|
|
383
|
+
if (this.peek() === "(") throw new NotLiteral(`a function call (${word}(...))`);
|
|
384
|
+
if (word === "for") throw new NotLiteral("a for-expression");
|
|
385
|
+
return this.reference(word);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Skip one value expression from the cursor: balance `()[]{}` and quoted
|
|
390
|
+
* strings (with their `${...}` interpolations), stop at a `,`, a newline, a
|
|
391
|
+
* comment or a closing bracket at depth 0. A heredoc is skipped to its
|
|
392
|
+
* closing marker line.
|
|
393
|
+
*/
|
|
394
|
+
private skipExpression(): void {
|
|
395
|
+
let depth = 0;
|
|
396
|
+
while (!this.atEnd()) {
|
|
397
|
+
const c = this.peek();
|
|
398
|
+
if (depth === 0) {
|
|
399
|
+
if (c === "," || c === "\n" || c === "}" || c === "]" || c === ")" || c === "#") return;
|
|
400
|
+
if (c === "/" && (this.src[this.pos + 1] === "/" || this.src[this.pos + 1] === "*")) return;
|
|
401
|
+
}
|
|
402
|
+
if (c === '"') this.skipQuoted();
|
|
403
|
+
else if (c === "<" && this.src.startsWith("<<", this.pos)) this.skipHeredoc();
|
|
404
|
+
else {
|
|
405
|
+
if (c === "(" || c === "[" || c === "{") depth++;
|
|
406
|
+
else if (c === ")" || c === "]" || c === "}") depth--;
|
|
407
|
+
this.pos++;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Skip a quoted string, including any `${...}` interpolations (which may hold strings). */
|
|
413
|
+
private skipQuoted(): void {
|
|
414
|
+
this.pos++;
|
|
415
|
+
while (!this.atEnd()) {
|
|
416
|
+
const c = this.peek();
|
|
417
|
+
if (c === "\\") this.pos += 2;
|
|
418
|
+
else if (c === '"') {
|
|
419
|
+
this.pos++;
|
|
420
|
+
return;
|
|
421
|
+
} else if ((c === "$" || c === "%") && this.src[this.pos + 1] === "{") {
|
|
422
|
+
this.pos += 2;
|
|
423
|
+
let depth = 1;
|
|
424
|
+
while (!this.atEnd() && depth > 0) {
|
|
425
|
+
const d = this.peek();
|
|
426
|
+
if (d === '"') {
|
|
427
|
+
this.skipQuoted();
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (d === "{") depth++;
|
|
431
|
+
else if (d === "}") depth--;
|
|
432
|
+
this.pos++;
|
|
433
|
+
}
|
|
434
|
+
} else this.pos++;
|
|
435
|
+
}
|
|
436
|
+
throw new Structural("an unterminated string");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private skipHeredoc(): void {
|
|
440
|
+
const head = /<<-?([A-Za-z_][A-Za-z0-9_-]*)[ \t]*\n/y;
|
|
441
|
+
head.lastIndex = this.pos;
|
|
442
|
+
const m = head.exec(this.src);
|
|
443
|
+
if (!m) throw new Structural("a malformed heredoc");
|
|
444
|
+
const close = new RegExp(`^[ \\t]*${m[1]}[ \\t]*$`, "m");
|
|
445
|
+
const rest = this.src.slice(this.pos + m[0].length);
|
|
446
|
+
const end = close.exec(rest);
|
|
447
|
+
if (!end) throw new Structural("an unterminated heredoc");
|
|
448
|
+
this.pos += m[0].length + end.index + end[0].length;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private reference(word: string): never {
|
|
452
|
+
throw new NotLiteral(`a reference (${word}${this.peek() === "." ? "..." : ""})`);
|
|
453
|
+
}
|
|
454
|
+
}
|
package/src/hcl/parse.test.ts
CHANGED
|
@@ -67,3 +67,28 @@ describe("blocksToEntities: suppression attachment (chant #2111)", () => {
|
|
|
67
67
|
expect(second.suppressions).toBeUndefined();
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
describe("Terragrunt config files (#3417)", () => {
|
|
72
|
+
const files = [
|
|
73
|
+
{
|
|
74
|
+
name: "terragrunt.hcl",
|
|
75
|
+
source: 'terraform {\n source = "../m"\n}\ndependency "vpc" {\n config_path = "../vpc"\n}\nremote_state {\n backend = "s3"\n}\n',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
test("terragrunt.hcl and root.hcl become Terragrunt entities, never module entities", async () => {
|
|
80
|
+
const entities = await blocksToEntities(files, "unit");
|
|
81
|
+
expect([...entities.values()].map((e) => e.entityType).sort()).toEqual([
|
|
82
|
+
"Terraform::TerragruntConfig",
|
|
83
|
+
"Terraform::TerragruntDependency",
|
|
84
|
+
"Terraform::TerragruntRemoteState",
|
|
85
|
+
"Terraform::TerragruntSource",
|
|
86
|
+
]);
|
|
87
|
+
expect([...entities.keys()]).toContain("unit/dependency.vpc");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("the same text under a .tf name is read as a Terraform terraform block", async () => {
|
|
91
|
+
const entities = await blocksToEntities([{ name: "main.tf", source: 'terraform {\n required_version = ">= 1.5"\n}\n' }], "unit");
|
|
92
|
+
expect([...entities.values()].map((e) => e.entityType)).toEqual(["Terraform::Terraform"]);
|
|
93
|
+
});
|
|
94
|
+
});
|
package/src/hcl/parse.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
22
22
|
import { join } from "node:path";
|
|
23
|
-
import { loadHcl2json, type Hcl2Json } from "@intentius/chant/terraform/
|
|
23
|
+
import { loadHcl2json, type Hcl2Json } from "@intentius/chant/terraform/hcl2json";
|
|
24
24
|
import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
|
|
25
25
|
import type { EntityReference } from "@intentius/chant/graph-ir";
|
|
26
26
|
import type { SuppressionDirective } from "@intentius/chant/lint/suppressions";
|
|
@@ -83,6 +83,13 @@ export interface TerraformEntity extends Declarable {
|
|
|
83
83
|
readonly mode?: TerraformRootMode;
|
|
84
84
|
/** The declared estate name. Present only when `mode` is `"live"`. */
|
|
85
85
|
readonly estate?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Whether `.terraform.lock.hcl` sits beside the root's `.tf` files (#3190,
|
|
88
|
+
* TF040). Stamped on every entity of a root parsed from a directory, and
|
|
89
|
+
* absent when the parse had no directory to look in (a fetched
|
|
90
|
+
* repository, inline source), which reads as "not determined".
|
|
91
|
+
*/
|
|
92
|
+
readonly lockFile?: boolean;
|
|
86
93
|
/** `terraform.roots.<name>.workspace`, recorded so TF025 can flag a non-default one on a live root. */
|
|
87
94
|
readonly workspace?: string;
|
|
88
95
|
/**
|
|
@@ -166,6 +173,26 @@ export const LOCALS_TYPE = "Terraform::Locals";
|
|
|
166
173
|
/** A `live { estate = "..." }` block, or an `estate.chdf.hcl` sidecar's content (#2103). */
|
|
167
174
|
export const LIVE_TYPE = "Terraform::Live";
|
|
168
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Terragrunt config entities (#3417). A `terragrunt.hcl` or `root.hcl` is not
|
|
178
|
+
* a Terraform module, so its blocks are read into these types and never into
|
|
179
|
+
* `TERRAFORM_TYPE`: TF001's `required_version` check and the rest of the
|
|
180
|
+
* module rules keep to the `.tf` files.
|
|
181
|
+
*/
|
|
182
|
+
export const TERRAGRUNT_CONFIG_TYPE = "Terraform::TerragruntConfig";
|
|
183
|
+
export const TERRAGRUNT_SOURCE_TYPE = "Terraform::TerragruntSource";
|
|
184
|
+
export const TERRAGRUNT_DEPENDENCY_TYPE = "Terraform::TerragruntDependency";
|
|
185
|
+
export const TERRAGRUNT_REMOTE_STATE_TYPE = "Terraform::TerragruntRemoteState";
|
|
186
|
+
export const TERRAGRUNT_GENERATE_TYPE = "Terraform::TerragruntGenerate";
|
|
187
|
+
|
|
188
|
+
/** The Terragrunt config files read, by their default names. A `--config` rename is not followed. */
|
|
189
|
+
export const TERRAGRUNT_FILENAMES: readonly string[] = ["root.hcl", "terragrunt.hcl"];
|
|
190
|
+
|
|
191
|
+
/** Whether `name` (a bare filename) is a Terragrunt config file this lexicon reads. */
|
|
192
|
+
export function isTerragruntFileName(name: string): boolean {
|
|
193
|
+
return TERRAGRUNT_FILENAMES.includes(name);
|
|
194
|
+
}
|
|
195
|
+
|
|
169
196
|
/** The sidecar filename choudoufu reads an estate declaration from when no in-block `live { }` is used. */
|
|
170
197
|
export const LIVE_SIDECAR_FILENAME = "estate.chdf.hcl";
|
|
171
198
|
|
|
@@ -260,6 +287,11 @@ export function listTerraformFiles(dir: string): string[] {
|
|
|
260
287
|
.sort();
|
|
261
288
|
}
|
|
262
289
|
|
|
290
|
+
/** The Terragrunt config files directly under `dir` (`root.hcl`, `terragrunt.hcl`), in filename order like the audit's bundle. */
|
|
291
|
+
export function listTerragruntFiles(dir: string): string[] {
|
|
292
|
+
return TERRAGRUNT_FILENAMES.filter((name) => existsSync(join(dir, name)));
|
|
293
|
+
}
|
|
294
|
+
|
|
263
295
|
/**
|
|
264
296
|
* Read the `estate.chdf.hcl` sidecar beside `dir`'s `.tf` files, if present.
|
|
265
297
|
* Sibling to {@link listTerraformFiles}: the sidecar is plain HCL (a bare
|
|
@@ -313,8 +345,13 @@ export interface TerraformRootModeOptions {
|
|
|
313
345
|
* declaration silently overrides.
|
|
314
346
|
*/
|
|
315
347
|
configEstate?: string;
|
|
348
|
+
/** Stamp `props.lockFile` on every entity (#3190). Set by the two directory-backed parses only. */
|
|
349
|
+
lockFile?: boolean;
|
|
316
350
|
}
|
|
317
351
|
|
|
352
|
+
/** The lock file `terraform init` writes beside a root's `.tf` files. */
|
|
353
|
+
export const LOCK_FILENAME = ".terraform.lock.hcl";
|
|
354
|
+
|
|
318
355
|
/**
|
|
319
356
|
* Parse a set of files into entities keyed `<root>/<address>`. Two blocks that
|
|
320
357
|
* genuinely share an address (two `locals` blocks, or the same address in two
|
|
@@ -351,8 +388,15 @@ export async function blocksToEntities(
|
|
|
351
388
|
const entities = new Map<string, Declarable>();
|
|
352
389
|
const prefix = callers.length > 0 ? `${root}/${callers.join("/")}` : root;
|
|
353
390
|
|
|
354
|
-
const add = (
|
|
355
|
-
|
|
391
|
+
const add = (
|
|
392
|
+
entityType: string,
|
|
393
|
+
address: string,
|
|
394
|
+
body: unknown,
|
|
395
|
+
file: TerraformFile,
|
|
396
|
+
scan: FileScan,
|
|
397
|
+
scanAddress: string = address,
|
|
398
|
+
): void => {
|
|
399
|
+
const { line, suppressions } = directivesFor(scan, scanAddress);
|
|
356
400
|
const entity = terraformEntity(
|
|
357
401
|
entityType,
|
|
358
402
|
address,
|
|
@@ -418,6 +462,10 @@ export async function blocksToEntities(
|
|
|
418
462
|
for (const file of files) {
|
|
419
463
|
const tree = (await parser.parse(file.name, file.source)) as Record<string, unknown>;
|
|
420
464
|
const scan = scanSuppressions(file.name, file.source);
|
|
465
|
+
if (isTerragruntFileName(file.name)) {
|
|
466
|
+
terragruntBlocks(tree, file, scan, add);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
421
469
|
unlabelled(tree, "terraform", TERRAFORM_TYPE, file, scan);
|
|
422
470
|
for (const liveBody of liveBlocksIn(tree)) add(LIVE_TYPE, "live", liveBody, file, scan);
|
|
423
471
|
unlabelled(tree, "locals", LOCALS_TYPE, file, scan);
|
|
@@ -450,7 +498,12 @@ export async function blocksToEntities(
|
|
|
450
498
|
const te = entity as TerraformEntity;
|
|
451
499
|
const stamped: TerraformEntity = {
|
|
452
500
|
...te,
|
|
453
|
-
props: {
|
|
501
|
+
props: {
|
|
502
|
+
...te.props,
|
|
503
|
+
mode,
|
|
504
|
+
...(mode === "live" ? { estate } : {}),
|
|
505
|
+
...(modeOptions?.lockFile !== undefined ? { lockFile: modeOptions.lockFile } : {}),
|
|
506
|
+
},
|
|
454
507
|
};
|
|
455
508
|
entities.set(key, stamped);
|
|
456
509
|
}
|
|
@@ -458,6 +511,34 @@ export async function blocksToEntities(
|
|
|
458
511
|
return entities;
|
|
459
512
|
}
|
|
460
513
|
|
|
514
|
+
/**
|
|
515
|
+
* One Terragrunt config file's blocks, as entities (#3417): the file's own
|
|
516
|
+
* settings (`terragrunt_version_constraint` and the `include` labels), the
|
|
517
|
+
* `terraform` block that names the unit's source, `remote_state`, each
|
|
518
|
+
* `dependency` and each `generate`. Everything else in the file (`inputs`,
|
|
519
|
+
* `locals`, `iam_role`) is left unread.
|
|
520
|
+
*/
|
|
521
|
+
function terragruntBlocks(
|
|
522
|
+
tree: Record<string, unknown>,
|
|
523
|
+
file: TerraformFile,
|
|
524
|
+
scan: FileScan,
|
|
525
|
+
add: (entityType: string, address: string, body: unknown, file: TerraformFile, scan: FileScan, scanAddress?: string) => void,
|
|
526
|
+
): void {
|
|
527
|
+
const config: BlockBody = { includes: Object.keys(asRecord(tree["include"])) };
|
|
528
|
+
if (typeof tree["terragrunt_version_constraint"] === "string") {
|
|
529
|
+
config["terragrunt_version_constraint"] = tree["terragrunt_version_constraint"];
|
|
530
|
+
}
|
|
531
|
+
add(TERRAGRUNT_CONFIG_TYPE, "terragrunt", config, file, scan, "");
|
|
532
|
+
for (const body of asArray(tree["terraform"])) add(TERRAGRUNT_SOURCE_TYPE, "terragrunt.terraform", body, file, scan, "terraform");
|
|
533
|
+
for (const body of asArray(tree["remote_state"])) add(TERRAGRUNT_REMOTE_STATE_TYPE, "remote_state", body, file, scan);
|
|
534
|
+
for (const [name, bodies] of Object.entries(asRecord(tree["dependency"]))) {
|
|
535
|
+
for (const body of asArray(bodies)) add(TERRAGRUNT_DEPENDENCY_TYPE, `dependency.${name}`, body, file, scan);
|
|
536
|
+
}
|
|
537
|
+
for (const [name, bodies] of Object.entries(asRecord(tree["generate"]))) {
|
|
538
|
+
for (const body of asArray(bodies)) add(TERRAGRUNT_GENERATE_TYPE, `generate.${name}`, body, file, scan);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
461
542
|
/**
|
|
462
543
|
* Parse a root module directory. Reads every `.tf` directly under `dir`, plus
|
|
463
544
|
* the `estate.chdf.hcl` sidecar beside them when present. Throws whatever the
|
|
@@ -471,7 +552,9 @@ export async function parseTerraformRootDir(
|
|
|
471
552
|
modeOptions?: TerraformRootModeOptions,
|
|
472
553
|
callers: readonly string[] = [],
|
|
473
554
|
): Promise<Map<string, Declarable>> {
|
|
474
|
-
|
|
555
|
+
// A child module is a Terraform module, never a Terragrunt unit (#3417).
|
|
556
|
+
const names = [...listTerraformFiles(dir), ...(callers.length === 0 ? listTerragruntFiles(dir) : [])];
|
|
557
|
+
const files = names.map((name) => ({
|
|
475
558
|
name,
|
|
476
559
|
source: readFileSync(join(dir, name), "utf-8"),
|
|
477
560
|
}));
|
|
@@ -479,7 +562,9 @@ export async function parseTerraformRootDir(
|
|
|
479
562
|
// estate a ROOT runs against, and choudoufu reads it from the root's own
|
|
480
563
|
// directory only. See `./descend.ts`.
|
|
481
564
|
const sidecar = callers.length === 0 ? readLiveSidecarFile(dir) : undefined;
|
|
482
|
-
|
|
565
|
+
// Only the root's own directory carries a lock file that matters (#3190).
|
|
566
|
+
const opts = callers.length === 0 ? { ...modeOptions, lockFile: existsSync(join(dir, LOCK_FILENAME)) } : modeOptions;
|
|
567
|
+
return blocksToEntities(files, root, hcl2json, opts, sidecar, callers);
|
|
483
568
|
}
|
|
484
569
|
|
|
485
570
|
/**
|
package/src/hcl/roots.ts
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { existsSync } from "node:fs";
|
|
30
30
|
import { isAbsolute, resolve } from "node:path";
|
|
31
31
|
import { isResourceDeclarable, type Declarable } from "@intentius/chant/declarable";
|
|
32
|
-
import type { Hcl2Json } from "@intentius/chant/terraform/
|
|
32
|
+
import type { Hcl2Json } from "@intentius/chant/terraform/hcl2json";
|
|
33
33
|
import type { TerraformRootConfig } from "../config";
|
|
34
34
|
import { LIVE_TYPE, parseTerraformRootDir } from "./parse";
|
|
35
35
|
import { descendModules, resolveCallModuleType, type CallModuleType } from "./descend";
|