@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,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pull-request loop (#3183) over five real Terraform roots, through the
|
|
3
|
+
* commands the generated CI runs: `chant components pr-plan` on the pull
|
|
4
|
+
* request and `chant components pr-apply` on the merge, each talking to a
|
|
5
|
+
* fake GitHub.
|
|
6
|
+
*
|
|
7
|
+
* The issue's "done when": a pull request touching one of five roots plans
|
|
8
|
+
* only that root and its dependents, applies on merge with the approved
|
|
9
|
+
* digest, and refuses with a clear message if the plan changed after review.
|
|
10
|
+
*
|
|
11
|
+
* The roots use `terraform_data`, which is built into `tofu` and `terraform`,
|
|
12
|
+
* with local state, so nothing downloads a provider or reaches a cloud. The
|
|
13
|
+
* estate is net -> a, b and a -> app, with dns on its own; a and b read net's
|
|
14
|
+
* output and app reads a's, through `stackOutput()`. Skipped when neither
|
|
15
|
+
* binary is on the PATH.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
|
|
23
|
+
import type { ParsedArgs } from "@intentius/chant/cli/registry";
|
|
24
|
+
import type { PrReport } from "@intentius/chant/pr-loop";
|
|
25
|
+
import { terraformPlugin } from "../plugin";
|
|
26
|
+
|
|
27
|
+
const REPO = join(import.meta.dirname, "..", "..", "..", "..");
|
|
28
|
+
|
|
29
|
+
function which(bin: string): boolean {
|
|
30
|
+
try {
|
|
31
|
+
execFileSync(bin, ["version"], { stdio: "ignore" });
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const BINARY = which("tofu") ? "tofu" : which("terraform") ? "terraform" : undefined;
|
|
38
|
+
|
|
39
|
+
const ROOTS: Record<string, string> = {
|
|
40
|
+
net: 'resource "terraform_data" "vpc" {\n input = "10.0.0.0/16"\n}\n\noutput "cidr" {\n value = terraform_data.vpc.output\n}\n',
|
|
41
|
+
a: 'variable "cidr" {\n type = string\n}\n\nresource "terraform_data" "subnet" {\n input = "${var.cidr}/a-v1"\n}\n\noutput "id" {\n value = terraform_data.subnet.output\n}\n',
|
|
42
|
+
b: 'variable "cidr" {\n type = string\n}\n\nresource "terraform_data" "subnet" {\n input = "${var.cidr}/b-v1"\n}\n',
|
|
43
|
+
app: 'variable "subnet" {\n type = string\n}\n\nresource "terraform_data" "app" {\n input = "app on ${var.subnet}"\n}\n',
|
|
44
|
+
dns: 'resource "terraform_data" "zone" {\n input = "example.test"\n}\n',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const COMPONENTS = `import { phase, stackOutput, type Component } from "@intentius/chant/components";
|
|
48
|
+
|
|
49
|
+
const apply = (root: string, vars?: Record<string, unknown>) =>
|
|
50
|
+
[phase("Apply", [{ kind: "terraform-apply", root, ...(vars ? { vars } : {}) }])];
|
|
51
|
+
|
|
52
|
+
export const net: Component = { name: "net", dependsOn: [], deploy: apply("net") };
|
|
53
|
+
export const a: Component = { name: "a", dependsOn: ["net"], deploy: apply("a", { cidr: stackOutput("net", "cidr") }) };
|
|
54
|
+
export const b: Component = { name: "b", dependsOn: ["net"], deploy: apply("b", { cidr: stackOutput("net", "cidr") }) };
|
|
55
|
+
export const app: Component = { name: "app", dependsOn: ["a"], deploy: apply("app", { subnet: stackOutput("a", "id") }) };
|
|
56
|
+
export const dns: Component = { name: "dns", dependsOn: [], deploy: apply("dns") };
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
/** GitHub as far as the loop uses it: comments, statuses, reviews and the merged pull request of a commit. */
|
|
60
|
+
class FakeGithub {
|
|
61
|
+
comments: Array<{ id: number; pr: number; body: string }> = [];
|
|
62
|
+
statuses: Array<{ sha: string; state: string; context: string; description: string }> = [];
|
|
63
|
+
reviews: Record<number, Array<{ user: { login: string }; state: string }>> = {};
|
|
64
|
+
mergedBy: Record<string, number> = {};
|
|
65
|
+
private nextId = 1;
|
|
66
|
+
|
|
67
|
+
fetch = async (url: string, init: { method: string; body?: string }) => {
|
|
68
|
+
const path = new URL(url).pathname.replace(/^\/repos\/acme\/infra/, "");
|
|
69
|
+
const body = init.body ? JSON.parse(init.body) : undefined;
|
|
70
|
+
const ok = (value: unknown) => ({ ok: true, status: 200, text: async () => JSON.stringify(value) });
|
|
71
|
+
let m: RegExpMatchArray | null;
|
|
72
|
+
if ((m = path.match(/^\/issues\/(\d+)\/comments$/))) {
|
|
73
|
+
const pr = Number(m[1]);
|
|
74
|
+
if (init.method === "GET") return ok(this.comments.filter((c) => c.pr === pr));
|
|
75
|
+
const c = { id: this.nextId++, pr, body: body.body };
|
|
76
|
+
this.comments.push(c);
|
|
77
|
+
return ok(c);
|
|
78
|
+
}
|
|
79
|
+
if ((m = path.match(/^\/issues\/comments\/(\d+)$/))) {
|
|
80
|
+
const c = this.comments.find((x) => x.id === Number(m![1]))!;
|
|
81
|
+
c.body = body.body;
|
|
82
|
+
return ok(c);
|
|
83
|
+
}
|
|
84
|
+
if ((m = path.match(/^\/statuses\/([0-9a-f]+)$/))) {
|
|
85
|
+
this.statuses.push({ sha: m[1], ...body });
|
|
86
|
+
return ok({});
|
|
87
|
+
}
|
|
88
|
+
if ((m = path.match(/^\/pulls\/(\d+)\/reviews$/))) return ok(this.reviews[Number(m[1])] ?? []);
|
|
89
|
+
if ((m = path.match(/^\/commits\/([0-9a-f]+)\/pulls$/))) {
|
|
90
|
+
const pr = this.mergedBy[m[1]];
|
|
91
|
+
return ok(pr ? [{ number: pr, merged_at: "2026-10-03T00:00:00Z", merge_commit_sha: m[1] }] : []);
|
|
92
|
+
}
|
|
93
|
+
return { ok: false, status: 404, text: async () => "" };
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const args = (overrides: Partial<ParsedArgs>): ParsedArgs => ({
|
|
98
|
+
command: "components",
|
|
99
|
+
path: ".",
|
|
100
|
+
format: "",
|
|
101
|
+
fix: false,
|
|
102
|
+
watch: false,
|
|
103
|
+
verbose: false,
|
|
104
|
+
help: false,
|
|
105
|
+
live: false,
|
|
106
|
+
env: "local",
|
|
107
|
+
noReleaseRecord: true,
|
|
108
|
+
...overrides,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe.skipIf(!BINARY)(`the pull-request loop over five ${BINARY ?? "terraform"} roots`, () => {
|
|
112
|
+
let dir: string;
|
|
113
|
+
let cwd: string;
|
|
114
|
+
const github = new FakeGithub();
|
|
115
|
+
const env = { ...process.env };
|
|
116
|
+
const git = (...a: string[]) => execFileSync("git", a, { cwd: dir, encoding: "utf8" }).trim();
|
|
117
|
+
const write = (path: string, text: string) => {
|
|
118
|
+
mkdirSync(dirname(join(dir, path)), { recursive: true });
|
|
119
|
+
writeFileSync(join(dir, path), text);
|
|
120
|
+
};
|
|
121
|
+
const report = (stage: "plan" | "apply"): PrReport => JSON.parse(readFileSync(join(dir, ".chant", "pr", `pr-${stage}.json`), "utf8"));
|
|
122
|
+
const state = (root: string) => readFileSync(join(dir, "roots", root, "terraform.tfstate"), "utf8");
|
|
123
|
+
const ctx = (overrides: Partial<ParsedArgs>) => ({ args: args(overrides), plugins: [terraformPlugin], serializers: [terraformPlugin.serializer] });
|
|
124
|
+
let handlers: typeof import("@intentius/chant/cli/handlers/pr");
|
|
125
|
+
let recordGateApproval: typeof import("@intentius/chant/cli/handlers/operator").recordGateApproval;
|
|
126
|
+
|
|
127
|
+
beforeAll(async () => {
|
|
128
|
+
dir = realpathSync(mkdtempSync(join(tmpdir(), "chant-pr-loop-")));
|
|
129
|
+
symlinkSync(join(REPO, "node_modules"), join(dir, "node_modules"));
|
|
130
|
+
for (const [name, text] of Object.entries(ROOTS)) write(`roots/${name}/main.tf`, text);
|
|
131
|
+
write(
|
|
132
|
+
"chant.config.json",
|
|
133
|
+
JSON.stringify({ lexicons: ["terraform"], terraform: { binary: BINARY, roots: Object.fromEntries(Object.keys(ROOTS).map((r) => [r, { dir: `roots/${r}` }])) } }, null, 2),
|
|
134
|
+
);
|
|
135
|
+
write("estate.component.ts", COMPONENTS);
|
|
136
|
+
write(".gitignore", "node_modules\n.terraform\n.terraform.lock.hcl\n*.tfstate*\nchant.tfplan\n.chant\n");
|
|
137
|
+
git("init", "-q", "-b", "main");
|
|
138
|
+
git("config", "user.email", "ci@example.test");
|
|
139
|
+
git("config", "user.name", "ci");
|
|
140
|
+
git("add", "-A");
|
|
141
|
+
git("commit", "-qm", "five roots");
|
|
142
|
+
|
|
143
|
+
cwd = process.cwd();
|
|
144
|
+
process.chdir(dir);
|
|
145
|
+
Object.assign(process.env, {
|
|
146
|
+
GITHUB_REPOSITORY: "acme/infra",
|
|
147
|
+
GITHUB_TOKEN: "t0k",
|
|
148
|
+
GITHUB_API_URL: "https://api.github.com",
|
|
149
|
+
GITHUB_SERVER_URL: "https://github.com",
|
|
150
|
+
GITHUB_RUN_ID: "1",
|
|
151
|
+
});
|
|
152
|
+
vi.stubGlobal("fetch", github.fetch);
|
|
153
|
+
handlers = await import("@intentius/chant/cli/handlers/pr");
|
|
154
|
+
({ recordGateApproval } = await import("@intentius/chant/cli/handlers/operator"));
|
|
155
|
+
|
|
156
|
+
// Every root applied once, so each has state and outputs to read.
|
|
157
|
+
const { runComponentsFanOut } = await import("@intentius/chant/cli/handlers/fan-out");
|
|
158
|
+
write("all.json", JSON.stringify({ changed: ["net", "dns"] }));
|
|
159
|
+
expect(await runComponentsFanOut(ctx({ fromAffected: "all.json" }) as never)).toBe(0);
|
|
160
|
+
}, 240_000);
|
|
161
|
+
|
|
162
|
+
afterAll(() => {
|
|
163
|
+
vi.unstubAllGlobals();
|
|
164
|
+
if (cwd) process.chdir(cwd);
|
|
165
|
+
for (const key of Object.keys(process.env)) if (!(key in env)) delete process.env[key];
|
|
166
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/** A pull request that rewrites root a's subnet to `version`, merged by `merge`. */
|
|
170
|
+
async function openPullRequest(pr: number, version: string): Promise<PrReport> {
|
|
171
|
+
git("checkout", "-qb", `pr-${pr}`);
|
|
172
|
+
write("roots/a/main.tf", ROOTS.a.replace("a-v1", version));
|
|
173
|
+
git("commit", "-qam", `a ${version}`);
|
|
174
|
+
expect(await handlers.runComponentsPrPlan(ctx({ base: "main", pr, forge: "github" }) as never)).toBe(0);
|
|
175
|
+
return report("plan");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function merge(pr: number): Promise<number> {
|
|
179
|
+
git("checkout", "-q", "main");
|
|
180
|
+
const before = git("rev-parse", "HEAD");
|
|
181
|
+
git("merge", "-q", "--no-ff", `pr-${pr}`, "-m", `Merge pull request #${pr}`);
|
|
182
|
+
github.mergedBy[git("rev-parse", "HEAD")] = pr;
|
|
183
|
+
return handlers.runComponentsPrApply(ctx({ base: before, forge: "github", requireReview: true }) as never);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
test("a change to one root plans that root and its dependent, and nothing else", async () => {
|
|
187
|
+
const plan = await openPullRequest(1, "a-v2");
|
|
188
|
+
|
|
189
|
+
expect(plan.status).toBe("planned");
|
|
190
|
+
expect(plan.selection).toMatchObject({ changed: ["a"], dependents: ["app"], waves: [["a"], ["app"]] });
|
|
191
|
+
expect(plan.members.map((m) => [m.member, m.counts.update])).toEqual([
|
|
192
|
+
["a", 1],
|
|
193
|
+
["app", 0],
|
|
194
|
+
]);
|
|
195
|
+
expect(plan.changeSet.digest).toBe(plan.digest);
|
|
196
|
+
expect(plan.approval).toEqual({ status: "pending" });
|
|
197
|
+
|
|
198
|
+
const note = github.comments.find((c) => c.pr === 1)!;
|
|
199
|
+
expect(note.body.startsWith("<!-- chant-pr:local -->")).toBe(true);
|
|
200
|
+
expect(note.body).toContain(`chant approve pr-1 pr-apply --plan ${plan.digest} --approver github:<your-login> --sign`);
|
|
201
|
+
expect(github.statuses.at(-1)).toMatchObject({ context: "chant/plan", state: "success", sha: plan.head });
|
|
202
|
+
}, 240_000);
|
|
203
|
+
|
|
204
|
+
test("on merge it applies the plan the reviewer approved", async () => {
|
|
205
|
+
const plan = report("plan");
|
|
206
|
+
github.reviews[1] = [{ user: { login: "alice" }, state: "APPROVED" }];
|
|
207
|
+
expect((await recordGateApproval("pr-1", "pr-apply", { actor: "github:alice", plan: plan.digest })).ok).toBe(true);
|
|
208
|
+
|
|
209
|
+
expect(await merge(1)).toBe(0);
|
|
210
|
+
const applied = report("apply");
|
|
211
|
+
expect(applied.digest).toBe(plan.digest);
|
|
212
|
+
expect(applied.status).toBe("applied");
|
|
213
|
+
expect(applied.approval).toEqual({ status: "approved", approvedBy: ["github:alice"] });
|
|
214
|
+
expect(applied.members.map((m) => [m.member, m.status, m.inputsMoved])).toEqual([
|
|
215
|
+
["a", "applied", undefined],
|
|
216
|
+
["app", "applied", ["a"]],
|
|
217
|
+
]);
|
|
218
|
+
expect(state("a")).toContain("10.0.0.0/16/a-v2");
|
|
219
|
+
// The same note, now with the apply's outcome.
|
|
220
|
+
expect(github.comments.filter((c) => c.pr === 1)).toHaveLength(1);
|
|
221
|
+
expect(github.comments.find((c) => c.pr === 1)!.body).toContain("### chant apply for `local`");
|
|
222
|
+
expect(github.statuses.at(-1)).toMatchObject({ context: "chant/apply", state: "success", sha: applied.head });
|
|
223
|
+
}, 240_000);
|
|
224
|
+
|
|
225
|
+
test("a plan that changed after review is refused, naming both digests, and nothing applies", async () => {
|
|
226
|
+
const reviewed = await openPullRequest(2, "a-v3");
|
|
227
|
+
github.reviews[2] = [{ user: { login: "alice" }, state: "APPROVED" }];
|
|
228
|
+
expect((await recordGateApproval("pr-2", "pr-apply", { actor: "github:alice", plan: reviewed.digest })).ok).toBe(true);
|
|
229
|
+
// Pushed after the review.
|
|
230
|
+
write("roots/a/main.tf", ROOTS.a.replace("a-v1", "a-v4"));
|
|
231
|
+
git("commit", "-qam", "a v4");
|
|
232
|
+
|
|
233
|
+
expect(await merge(2)).toBe(3);
|
|
234
|
+
const refused = report("apply");
|
|
235
|
+
expect(refused.status).toBe("refused");
|
|
236
|
+
expect(refused.refusal).toBe("plan-changed");
|
|
237
|
+
expect(refused.approval).toEqual({ status: "changed", approved: reviewed.digest });
|
|
238
|
+
expect(refused.message).toContain(`approved: ${reviewed.digest}; planned now: ${refused.digest}`);
|
|
239
|
+
expect(refused.message).toContain("nothing was applied");
|
|
240
|
+
expect(state("a")).toContain("a-v2");
|
|
241
|
+
expect(state("a")).not.toContain("a-v4");
|
|
242
|
+
expect(github.statuses.at(-1)).toMatchObject({ context: "chant/apply", state: "failure", description: "refused: the plan changed after review" });
|
|
243
|
+
}, 240_000);
|
|
244
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { validateActivitySteps, type ActivityStep, type OpConfig } from "@intentius/chant/op";
|
|
3
|
+
import { TerraformPinRolloutOp } from "./terraform-pin-rollout-op";
|
|
4
|
+
import { terraformPinRolloutContract } from "../op/activity-contracts";
|
|
5
|
+
import { parsePinRolloutArgs, pinRolloutExitCode, terraformCommands } from "../commands";
|
|
6
|
+
|
|
7
|
+
function props(config: Parameters<typeof TerraformPinRolloutOp>[0]): OpConfig {
|
|
8
|
+
return (TerraformPinRolloutOp(config).op as unknown as { props: OpConfig }).props;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const BASE = { name: "vpc-1-4", module: "oci://registry.example.com/modules/vpc", from: "1.3.0", to: "1.4.0" };
|
|
12
|
+
|
|
13
|
+
describe("TerraformPinRolloutOp (#3189)", () => {
|
|
14
|
+
test("one Rollout phase, one step, the rollout's status as the run outcome", () => {
|
|
15
|
+
const op = props({ ...BASE, canaries: ["live/dev/vpc"], mode: "pull-request", schedule: "*/30 * * * *" });
|
|
16
|
+
expect(op.phases.map((p) => p.name)).toEqual(["Rollout"]);
|
|
17
|
+
const step = op.phases[0]!.steps[0] as ActivityStep;
|
|
18
|
+
expect(step.fn).toBe("terraformPinRollout");
|
|
19
|
+
expect(step.args).toEqual({ module: BASE.module, from: "1.3.0", to: "1.4.0", canaries: ["live/dev/vpc"], mode: "pull-request" });
|
|
20
|
+
expect(step.outcomeAttribute).toEqual({ name: "Rollout", from: "status" });
|
|
21
|
+
expect(op.schedule).toEqual({ cron: "*/30 * * * *", overlap: "skip" });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("its step validates against the activity's contract", () => {
|
|
25
|
+
const op = props({ ...BASE, roots: [{ root: "a" }, { root: "b", dependsOn: ["a"] }] });
|
|
26
|
+
const contracts = new Map([["terraformPinRollout", terraformPinRolloutContract]]);
|
|
27
|
+
expect(validateActivitySteps(op, contracts)).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("chant terraform pin-rollout", () => {
|
|
32
|
+
test("is mounted as the terraform group's pin-rollout verb", () => {
|
|
33
|
+
expect(terraformCommands.name).toBe("terraform");
|
|
34
|
+
expect(terraformCommands.commands.map((c) => c.name)).toEqual(["pin-rollout"]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("parses roots, dependencies, a generated root and canaries into the activity's args", () => {
|
|
38
|
+
const { args, json } = parsePinRolloutArgs([
|
|
39
|
+
"--module=oci://r/m/vpc",
|
|
40
|
+
"--from", "1.3.0",
|
|
41
|
+
"--to", "1.4.0",
|
|
42
|
+
"--root", "a",
|
|
43
|
+
"--depends-on", "b=a",
|
|
44
|
+
"--ts-source", "c=c/vpc.ts",
|
|
45
|
+
"--canary", "a",
|
|
46
|
+
"--pull-request",
|
|
47
|
+
"--json",
|
|
48
|
+
]);
|
|
49
|
+
expect(json).toBe(true);
|
|
50
|
+
expect(args).toEqual({
|
|
51
|
+
module: "oci://r/m/vpc",
|
|
52
|
+
from: "1.3.0",
|
|
53
|
+
to: "1.4.0",
|
|
54
|
+
mode: "pull-request",
|
|
55
|
+
roots: [{ root: "a" }, { root: "b", dependsOn: ["a"] }, { root: "c", tsSource: "c/vpc.ts" }],
|
|
56
|
+
canaries: ["a"],
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("report is the default mode, and the required flags are required", () => {
|
|
61
|
+
expect(parsePinRolloutArgs(["--module", "m", "--from", "1", "--to", "2"]).args.mode).toBe("report");
|
|
62
|
+
expect(() => parsePinRolloutArgs(["--module", "m", "--from", "1"])).toThrow(/--to is required/);
|
|
63
|
+
expect(() => parsePinRolloutArgs(["--module", "m", "--from", "1", "--to", "2", "--bogus"])).toThrow(/Unknown flag: --bogus/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("waiting exits 3, stopped exits 1, the rest 0", () => {
|
|
67
|
+
expect(["complete", "opened", "would-open", "waiting", "stopped"].map(pinRolloutExitCode)).toEqual([0, 0, 0, 3, 1]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TerraformPinRolloutOp composite (#3189): roll a module version out as
|
|
3
|
+
* pin-bump pull requests, one wave at a time.
|
|
4
|
+
*
|
|
5
|
+
* One phase, one `terraformPinRollout` step. Each run reads the rollout's
|
|
6
|
+
* state from the forge and opens at most the next wave's PR, so the Op is
|
|
7
|
+
* meant to run again: on a `schedule`, or from the pipeline that applies a
|
|
8
|
+
* merged wave. Wave N+1 opens only on a run after wave N's PR merged and each
|
|
9
|
+
* of its roots reported its apply check as passed on the merge commit. Until
|
|
10
|
+
* then a run reports where the rollout stands and opens nothing. A failed
|
|
11
|
+
* root fails the run, naming it.
|
|
12
|
+
*
|
|
13
|
+
* The run's `Rollout` outcome is the rollout's status: `opened`, `waiting`,
|
|
14
|
+
* `complete`, or `would-open` in report mode.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { TerraformPinRolloutOp } from "@intentius/chant-lexicon-terraform";
|
|
19
|
+
*
|
|
20
|
+
* export const { op } = TerraformPinRolloutOp({
|
|
21
|
+
* name: "vpc-1-4",
|
|
22
|
+
* module: "oci://registry.example.com/modules/vpc",
|
|
23
|
+
* from: "1.3.0",
|
|
24
|
+
* to: "1.4.0",
|
|
25
|
+
* canaries: ["live/dev/vpc"],
|
|
26
|
+
* mode: "pull-request",
|
|
27
|
+
* schedule: "*\/30 * * * *",
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { Op, phase, OpResource } from "@intentius/chant/op";
|
|
33
|
+
import { terraformPinRollout } from "../op/builders";
|
|
34
|
+
import type { TerraformPinRolloutArgs } from "../op/activities/pin-rollout";
|
|
35
|
+
|
|
36
|
+
export interface TerraformPinRolloutOpConfig extends TerraformPinRolloutArgs {
|
|
37
|
+
/** Op name (kebab-case). */
|
|
38
|
+
name: string;
|
|
39
|
+
/** Cron expression, so the rollout re-reads the forge and moves on without anyone running it. */
|
|
40
|
+
schedule?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TerraformPinRolloutOpResources {
|
|
44
|
+
/** Op resource. One Rollout phase. */
|
|
45
|
+
op: InstanceType<typeof OpResource>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function TerraformPinRolloutOp(config: TerraformPinRolloutOpConfig): TerraformPinRolloutOpResources {
|
|
49
|
+
const { name, schedule, ...args } = config;
|
|
50
|
+
const step = terraformPinRollout(args);
|
|
51
|
+
step.outcomeAttribute = { name: "Rollout", from: "status" };
|
|
52
|
+
const op = Op({
|
|
53
|
+
name,
|
|
54
|
+
overview: `Roll ${config.module} ${config.from} -> ${config.to} out as one pin-bump pull request per wave`,
|
|
55
|
+
labels: { Surface: "terraform-pin-rollout" },
|
|
56
|
+
...(schedule ? { schedule: { cron: schedule, overlap: "skip" as const } } : {}),
|
|
57
|
+
phases: [phase("Rollout", [step])],
|
|
58
|
+
});
|
|
59
|
+
return { op };
|
|
60
|
+
}
|
package/src/hcl/descend.ts
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
import { existsSync, statSync } from "node:fs";
|
|
50
50
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
51
51
|
import type { Declarable } from "@intentius/chant/declarable";
|
|
52
|
-
import type { Hcl2Json } from "@intentius/chant/terraform/
|
|
52
|
+
import type { Hcl2Json } from "@intentius/chant/terraform/hcl2json";
|
|
53
53
|
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
54
54
|
import { classifyModuleSource } from "../lint/post-synth/module-source";
|
|
55
55
|
import {
|
package/src/hcl/edges.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* The `${...}` body is tokenized by hcl2json's own expression AST
|
|
16
16
|
* (`getReferencesInExpression`), never by a regex over the string. That is the
|
|
17
|
-
* same instrument `packages/core/src/terraform/
|
|
17
|
+
* same instrument `packages/core/src/terraform/hcl2json.ts` uses for the carve-out
|
|
18
18
|
* advisor, and for the same reason: a quoted address used as a map key
|
|
19
19
|
* (`var.m["aws_s3_bucket.assets.arn"]`) and an escaped `$${...}` literal are
|
|
20
20
|
* not references, and no regex over the raw string can tell. `./references.ts`
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
|
|
87
87
|
import { isResourceDeclarable, type Declarable } from "@intentius/chant/declarable";
|
|
88
88
|
import type { EntityReference } from "@intentius/chant/graph-ir";
|
|
89
|
-
import { loadHcl2json, type Hcl2Json } from "@intentius/chant/terraform/
|
|
89
|
+
import { loadHcl2json, type Hcl2Json } from "@intentius/chant/terraform/hcl2json";
|
|
90
90
|
import { refFromAccessor } from "@intentius/chant/terraform/graph";
|
|
91
91
|
import { LOCALS_TYPE, scopeOfKey, type BlockBody, type TerraformEntity } from "./parse";
|
|
92
92
|
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { hasUnknownKeys, isUnknown, readJsonencode, UNKNOWN, UNKNOWN_KEYS } from "./jsonencode";
|
|
3
|
+
|
|
4
|
+
/** What hcl2json hands back for `attr = jsonencode(<arg>)`: the source text, wrapped as one interpolation. */
|
|
5
|
+
const call = (arg: string): string => `\${jsonencode(${arg})}`;
|
|
6
|
+
|
|
7
|
+
describe("readJsonencode: a literal jsonencode argument read as structure (chant #2286)", () => {
|
|
8
|
+
test("an IAM policy in HCL object syntax, as hcl2json renders it", () => {
|
|
9
|
+
const raw =
|
|
10
|
+
'${jsonencode({\n Version = "2012-10-17"\n Statement = [\n {\n Action = ["s3:*", "ec2:Describe*"]\n' +
|
|
11
|
+
' "Effect" : "Allow"\n Resource = "*"\n },\n ]\n })}';
|
|
12
|
+
expect(readJsonencode(raw)).toEqual({
|
|
13
|
+
kind: "literal",
|
|
14
|
+
value: {
|
|
15
|
+
Version: "2012-10-17",
|
|
16
|
+
Statement: [{ Action: ["s3:*", "ec2:Describe*"], Effect: "Allow", Resource: "*" }],
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("a container definitions tuple with commas, numbers, booleans and null", () => {
|
|
22
|
+
const raw = call('[{ name = "app", cpu = 256, essential = true, memory = null, portMappings = [{ containerPort = 80 }] }]');
|
|
23
|
+
expect(readJsonencode(raw)).toEqual({
|
|
24
|
+
kind: "literal",
|
|
25
|
+
value: [{ name: "app", cpu: 256, essential: true, memory: null, portMappings: [{ containerPort: 80 }] }],
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("scalars, negative and exponent numbers, and string escapes", () => {
|
|
30
|
+
expect(readJsonencode(call('"x"'))).toEqual({ kind: "literal", value: "x" });
|
|
31
|
+
expect(readJsonencode(call("-1.5e2"))).toEqual({ kind: "literal", value: -150 });
|
|
32
|
+
expect(readJsonencode(call('"a\\"b\\\\c\\n\\u00e9"'))).toEqual({ kind: "literal", value: 'a"b\\c\né' });
|
|
33
|
+
expect(readJsonencode(call('"$${literal} %%{also}"'))).toEqual({ kind: "literal", value: "${literal} %{also}" });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("comments inside the argument are whitespace", () => {
|
|
37
|
+
const raw = call('{\n # why\n a = 1 // trailing\n /* block */ b = "x"\n}');
|
|
38
|
+
expect(readJsonencode(raw)).toEqual({ kind: "literal", value: { a: 1, b: "x" } });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("spacing around the call itself", () => {
|
|
42
|
+
expect(readJsonencode('${ jsonencode ( { a = 1 } ) }')).toEqual({ kind: "literal", value: { a: 1 } });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test.each([
|
|
46
|
+
["a reference as a value", '{ Resource = var.arn }', /reference/],
|
|
47
|
+
["a reference as the whole argument", "local.policy", /reference/],
|
|
48
|
+
["a function call", '{ Resource = concat(["a"], var.b) }', /function call/],
|
|
49
|
+
["a for-expression in a tuple", "[for a in var.actions : a]", /for-expression/],
|
|
50
|
+
["a for-expression in an object", "{ for k, v in var.m : k => v }", /for-expression/],
|
|
51
|
+
["a conditional", '{ Action = var.admin ? "*" : "s3:GetObject" }', /reference/],
|
|
52
|
+
["a conditional on a literal", '{ Action = true ? "*" : "x" }', /conditional/],
|
|
53
|
+
["an interpolated string", '{ Resource = "arn:aws:s3:::${var.bucket}/*" }', /interpolation/],
|
|
54
|
+
["a template directive", '{ Resource = "%{ if true }x%{ endif }" }', /directive/],
|
|
55
|
+
["an operator", "{ n = 1 + 2 }", /operator/],
|
|
56
|
+
["a computed key", '{ (var.k) = "v" }', /computed object key/],
|
|
57
|
+
["a heredoc", "<<EOT\n{}\nEOT\n", /heredoc/],
|
|
58
|
+
["an index on a literal", '{ a = ["x"][0] }', /index/],
|
|
59
|
+
])("not-determined: %s", (_name, arg, reason) => {
|
|
60
|
+
const read = readJsonencode(call(arg));
|
|
61
|
+
expect(read.kind).toBe("not-determined");
|
|
62
|
+
if (read.kind === "not-determined") expect(read.reason).toMatch(reason);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("not-determined: two calls in one template, never literal", () => {
|
|
66
|
+
expect(readJsonencode('${jsonencode({a = 1})}-${jsonencode({b = 2})}').kind).toBe("not-determined");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test.each([
|
|
70
|
+
["a plain string", '{"Statement":[]}'],
|
|
71
|
+
["a reference", "${data.aws_iam_policy_document.x.json}"],
|
|
72
|
+
["a file() call", '${file("policy.json")}'],
|
|
73
|
+
["a templatefile() call", '${templatefile("p.tpl", { a = 1 })}'],
|
|
74
|
+
["a template around a jsonencode call", 'x-${jsonencode({a = 1})}'],
|
|
75
|
+
["a number", 3],
|
|
76
|
+
["absent", undefined],
|
|
77
|
+
["a structured value", { Statement: [] }],
|
|
78
|
+
])("not-jsonencode: %s", (_name, raw) => {
|
|
79
|
+
expect(readJsonencode(raw)).toEqual({ kind: "not-jsonencode" });
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("readJsonencode with unknownLeaves: expressions become opaque leaves", () => {
|
|
84
|
+
const leaves = (raw: string) => readJsonencode(raw, { unknownLeaves: true });
|
|
85
|
+
|
|
86
|
+
test("a literal Action survives beside a reference Resource", () => {
|
|
87
|
+
const read = leaves(call('{\n Statement = [{\n Action = "*"\n Resource = aws_s3_bucket.x.arn\n Effect = "Allow"\n }]\n}'));
|
|
88
|
+
expect(read.kind).toBe("literal");
|
|
89
|
+
if (read.kind !== "literal") return;
|
|
90
|
+
const stmt = (read.value as { Statement: Record<string, unknown>[] }).Statement[0];
|
|
91
|
+
expect(stmt.Action).toBe("*");
|
|
92
|
+
expect(stmt.Effect).toBe("Allow");
|
|
93
|
+
expect(isUnknown(stmt.Resource)).toBe(true);
|
|
94
|
+
expect((stmt.Resource as { reason: string }).reason).toMatch(/reference/);
|
|
95
|
+
expect((stmt.Resource as Record<symbol, unknown>)[UNKNOWN]).toBe(true);
|
|
96
|
+
expect((stmt.Resource as { source: string }).source).toBe("aws_s3_bucket.x.arn");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("container definitions keep their literal environment beside image = var.image", () => {
|
|
100
|
+
const read = leaves(
|
|
101
|
+
call('[{\n name = "app"\n image = "${var.repo}:${var.tag}"\n cpu = var.cpu * 2\n environment = [{ name = "DB_PASSWORD", value = "hunter2" }]\n}]'),
|
|
102
|
+
);
|
|
103
|
+
expect(read.kind).toBe("literal");
|
|
104
|
+
if (read.kind !== "literal") return;
|
|
105
|
+
const def = (read.value as Record<string, unknown>[])[0];
|
|
106
|
+
expect(def.name).toBe("app");
|
|
107
|
+
expect(isUnknown(def.image)).toBe(true);
|
|
108
|
+
expect((def.image as { source: string }).source).toBe('"${var.repo}:${var.tag}"');
|
|
109
|
+
expect(isUnknown(def.cpu)).toBe(true);
|
|
110
|
+
expect(def.environment).toEqual([{ name: "DB_PASSWORD", value: "hunter2" }]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test.each([
|
|
114
|
+
["a function call spanning lines", 'merge(\n { a = "x" },\n var.extra,\n)'],
|
|
115
|
+
["a conditional with nested literals", 'var.admin ? { a = "*" } : {}'],
|
|
116
|
+
["a heredoc", "<<EOT\n{\"a\": 1}\nEOT"],
|
|
117
|
+
["an indexed tuple", '["x"][0]'],
|
|
118
|
+
["a splat", "aws_s3_bucket.b[*].arn"],
|
|
119
|
+
["a string with nested quotes in its interpolation", '"${lookup(var.m, "k", "}")}"'],
|
|
120
|
+
])("skips %s and reads the next field", (_name, expr) => {
|
|
121
|
+
const read = leaves(call(`{\n a = ${expr}\n b = "after"\n}`));
|
|
122
|
+
expect(read.kind).toBe("literal");
|
|
123
|
+
if (read.kind !== "literal") return;
|
|
124
|
+
const v = read.value as Record<string, unknown>;
|
|
125
|
+
expect(isUnknown(v.a)).toBe(true);
|
|
126
|
+
expect(v.b).toBe("after");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("tuple items are skipped one at a time", () => {
|
|
130
|
+
const read = leaves(call('["s3:GetObject", var.extra, "*"]'));
|
|
131
|
+
expect(read.kind).toBe("literal");
|
|
132
|
+
if (read.kind !== "literal") return;
|
|
133
|
+
const v = read.value as unknown[];
|
|
134
|
+
expect(v[0]).toBe("s3:GetObject");
|
|
135
|
+
expect(isUnknown(v[1])).toBe(true);
|
|
136
|
+
expect(v[2]).toBe("*");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test.each([
|
|
140
|
+
["a tuple for-expression as the whole argument", "[for s in var.s : s]", /for-expression/],
|
|
141
|
+
["an object for-expression as the whole argument", "{ for k, v in var.m : k => v }", /for-expression/],
|
|
142
|
+
["an expression as the whole argument", "local.policy", /reference/],
|
|
143
|
+
["a splat as the whole argument", "var.x[*]", /reference/],
|
|
144
|
+
])("still not-determined: %s", (_name, arg, reason) => {
|
|
145
|
+
const read = leaves(call(arg));
|
|
146
|
+
expect(read.kind).toBe("not-determined");
|
|
147
|
+
if (read.kind === "not-determined") expect(read.reason).toMatch(reason);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("the default mode is unchanged: one expression makes the whole read not-determined", () => {
|
|
151
|
+
expect(readJsonencode(call('{ Action = "*", Resource = var.arn }')).kind).toBe("not-determined");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("readJsonencode with unknownLeaves: members with an expression key", () => {
|
|
156
|
+
test("a traefik label keyed by an interpolation is dropped and recorded (govuk-forms/forms-admin shape)", () => {
|
|
157
|
+
const raw = call(
|
|
158
|
+
'[{\n name = "forms-admin"\n dockerLabels = {\n "traefik.enable" : "true"\n' +
|
|
159
|
+
' "traefik.http.routers.forms-admin-pr-${var.pull_request_number}.rule" : "Host(`pr-${var.pull_request_number}.example`)"\n' +
|
|
160
|
+
' "traefik.port" : "3000"\n }\n}]',
|
|
161
|
+
);
|
|
162
|
+
const read = readJsonencode(raw, { unknownLeaves: true });
|
|
163
|
+
expect(read.kind).toBe("literal");
|
|
164
|
+
if (read.kind !== "literal") return;
|
|
165
|
+
const labels = (read.value as Record<string, unknown>[])[0].dockerLabels as Record<string | symbol, unknown>;
|
|
166
|
+
expect(Object.keys(labels)).toEqual(["traefik.enable", "traefik.port"]);
|
|
167
|
+
expect(hasUnknownKeys(labels)).toBe(true);
|
|
168
|
+
expect(labels[UNKNOWN_KEYS]).toEqual([expect.stringMatching(/object key holding .*interpolation/)]);
|
|
169
|
+
expect(hasUnknownKeys((read.value as Record<string, unknown>[])[0])).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("a parenthesised computed key is dropped with its multi-line value", () => {
|
|
173
|
+
const read = readJsonencode(call('{\n (var.k) = {\n x = 1\n }\n b = "after"\n}'), { unknownLeaves: true });
|
|
174
|
+
expect(read.kind).toBe("literal");
|
|
175
|
+
if (read.kind !== "literal") return;
|
|
176
|
+
expect(Object.keys(read.value as object)).toEqual(["b"]);
|
|
177
|
+
expect((read.value as Record<symbol, unknown>)[UNKNOWN_KEYS]).toEqual(["a computed object key"]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test.each([
|
|
181
|
+
["a computed key", '{ (var.k) = "v" }', /computed object key/],
|
|
182
|
+
["an interpolated key", '{ "${var.k}" = "v" }', /object key/],
|
|
183
|
+
])("default mode: %s makes the whole read not-determined", (_name, arg, reason) => {
|
|
184
|
+
const read = readJsonencode(call(arg));
|
|
185
|
+
expect(read.kind).toBe("not-determined");
|
|
186
|
+
if (read.kind === "not-determined") expect(read.reason).toMatch(reason);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("readJsonencode with unknownLeaves: a for-expression below the argument is a leaf", () => {
|
|
191
|
+
test("a tuple for-expression as a member value (sonodar/dify-aws-terraform shape)", () => {
|
|
192
|
+
const raw = call(
|
|
193
|
+
'[{\n name = "api"\n environment = [for name, value in {\n MODE = "api"\n LOG_LEVEL = "INFO"\n } : { name = name, value = tostring(value) }]\n essential = true\n}]',
|
|
194
|
+
);
|
|
195
|
+
const read = readJsonencode(raw, { unknownLeaves: true });
|
|
196
|
+
expect(read.kind).toBe("literal");
|
|
197
|
+
if (read.kind !== "literal") return;
|
|
198
|
+
const def = (read.value as Record<string, unknown>[])[0];
|
|
199
|
+
expect(def.name).toBe("api");
|
|
200
|
+
expect(isUnknown(def.environment)).toBe(true);
|
|
201
|
+
expect((def.environment as { reason: string }).reason).toMatch(/for-expression/);
|
|
202
|
+
expect(def.essential).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("an object for-expression as a member value", () => {
|
|
206
|
+
const read = readJsonencode(call('{\n labels = { for k, v in var.m : k => v }\n b = 1\n}'), { unknownLeaves: true });
|
|
207
|
+
expect(read.kind).toBe("literal");
|
|
208
|
+
if (read.kind !== "literal") return;
|
|
209
|
+
const v = read.value as Record<string, unknown>;
|
|
210
|
+
expect(isUnknown(v.labels)).toBe(true);
|
|
211
|
+
expect(v.b).toBe(1);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("a for-expression producing a policy's Statement list is one unknown leaf", () => {
|
|
215
|
+
const read = readJsonencode(call('{\n Version = "2012-10-17"\n Statement = [for s in var.statements : s]\n}'), { unknownLeaves: true });
|
|
216
|
+
expect(read.kind).toBe("literal");
|
|
217
|
+
if (read.kind !== "literal") return;
|
|
218
|
+
expect(isUnknown((read.value as Record<string, unknown>).Statement)).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("a for-expression as a tuple item", () => {
|
|
222
|
+
const read = readJsonencode(call('["a", [for x in var.y : x]]'), { unknownLeaves: true });
|
|
223
|
+
expect(read.kind).toBe("literal");
|
|
224
|
+
if (read.kind === "literal") expect(isUnknown((read.value as unknown[])[1])).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
});
|