@intentius/chant-lexicon-terraform 0.124.52 → 0.124.55
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/README.md +14 -10
- package/dist/__fixtures__/pr-loop/estate.component.d.ts +7 -0
- package/dist/__fixtures__/pr-loop/estate.component.d.ts.map +1 -0
- package/dist/__fixtures__/pr-loop/estate.component.js +7 -0
- package/dist/__fixtures__/report/generate.d.ts +26 -0
- package/dist/__fixtures__/report/generate.d.ts.map +1 -0
- package/dist/__fixtures__/report/generate.js +58 -0
- package/dist/applied-state.d.ts +27 -0
- package/dist/applied-state.d.ts.map +1 -0
- package/dist/applied-state.js +44 -0
- package/dist/backend-config.d.ts +53 -0
- package/dist/backend-config.d.ts.map +1 -0
- package/dist/backend-config.js +136 -0
- package/dist/behaviour/kinds.d.ts.map +1 -1
- package/dist/behaviour/kinds.js +4 -0
- package/dist/behaviour/predict.d.ts.map +1 -1
- package/dist/behaviour/predict.js +6 -2
- package/dist/behaviour/reference-catalog.d.ts +16 -5
- package/dist/behaviour/reference-catalog.d.ts.map +1 -1
- package/dist/behaviour/reference-catalog.js +16 -5
- package/dist/behaviour/request.d.ts.map +1 -1
- package/dist/behaviour/request.js +3 -2
- package/dist/changed-roots.d.ts +6 -4
- package/dist/changed-roots.d.ts.map +1 -1
- package/dist/changed-roots.js +14 -18
- package/dist/ci-setup.d.ts +11 -0
- package/dist/ci-setup.d.ts.map +1 -0
- package/dist/ci-setup.js +41 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +4 -3
- package/dist/components/capability-plugin.d.ts +105 -9
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +357 -36
- package/dist/components/compose-group.d.ts +145 -0
- package/dist/components/compose-group.d.ts.map +1 -0
- package/dist/components/compose-group.js +407 -0
- package/dist/components/placeholder-preview.d.ts +78 -0
- package/dist/components/placeholder-preview.d.ts.map +1 -0
- package/dist/components/placeholder-preview.js +254 -0
- package/dist/components/release-inputs.d.ts +29 -0
- package/dist/components/release-inputs.d.ts.map +1 -0
- package/dist/components/release-inputs.js +57 -0
- package/dist/components/trace-facts.d.ts +16 -0
- package/dist/components/trace-facts.d.ts.map +1 -0
- package/dist/components/trace-facts.js +19 -0
- package/dist/compose/apply-events.d.ts +46 -0
- package/dist/compose/apply-events.d.ts.map +1 -0
- package/dist/compose/apply-events.js +77 -0
- package/dist/compose/estate.d.ts +194 -0
- package/dist/compose/estate.d.ts.map +1 -0
- package/dist/compose/estate.js +363 -0
- package/dist/compose/hcl.d.ts +99 -0
- package/dist/compose/hcl.d.ts.map +1 -0
- package/dist/compose/hcl.js +302 -0
- package/dist/compose/module-address.d.ts +24 -0
- package/dist/compose/module-address.d.ts.map +1 -0
- package/dist/compose/module-address.js +43 -0
- package/dist/compose/root.d.ts +127 -0
- package/dist/compose/root.d.ts.map +1 -0
- package/dist/compose/root.js +586 -0
- package/dist/compose/run.d.ts +183 -0
- package/dist/compose/run.d.ts.map +1 -0
- package/dist/compose/run.js +447 -0
- package/dist/compose/slice.d.ts +102 -0
- package/dist/compose/slice.d.ts.map +1 -0
- package/dist/compose/slice.js +249 -0
- package/dist/compose/state-address.d.ts +28 -0
- package/dist/compose/state-address.d.ts.map +1 -0
- package/dist/compose/state-address.js +154 -0
- package/dist/compose/state-merge.d.ts +52 -0
- package/dist/compose/state-merge.d.ts.map +1 -0
- package/dist/compose/state-merge.js +135 -0
- package/dist/compose.d.ts +179 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +482 -0
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-apply-op.js +7 -3
- package/dist/cone.d.ts +53 -0
- package/dist/cone.d.ts.map +1 -0
- package/dist/cone.js +119 -0
- package/dist/config.d.ts +59 -13
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -9
- package/dist/cost-hold.d.ts +27 -0
- package/dist/cost-hold.d.ts.map +1 -0
- package/dist/cost-hold.js +21 -0
- package/dist/describe-resources.d.ts +74 -12
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/describe-resources.js +374 -66
- package/dist/drift-read.d.ts +33 -0
- package/dist/drift-read.d.ts.map +1 -0
- package/dist/drift-read.js +243 -0
- package/dist/drift-record.d.ts +147 -0
- package/dist/drift-record.d.ts.map +1 -0
- package/dist/drift-record.js +383 -0
- package/dist/drift-rows.d.ts +61 -0
- package/dist/drift-rows.d.ts.map +1 -0
- package/dist/drift-rows.js +108 -0
- package/dist/exit-codes.d.ts +55 -0
- package/dist/exit-codes.d.ts.map +1 -0
- package/dist/exit-codes.js +48 -0
- package/dist/hcl/edges.d.ts.map +1 -1
- package/dist/hcl/edges.js +24 -1
- package/dist/hcl/files.d.ts +61 -0
- package/dist/hcl/files.d.ts.map +1 -0
- package/dist/hcl/files.js +221 -0
- package/dist/hcl/instances.d.ts +82 -0
- package/dist/hcl/instances.d.ts.map +1 -0
- package/dist/hcl/instances.js +390 -0
- package/dist/hcl/parse.d.ts +81 -9
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/parse.js +114 -16
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/roots.js +5 -0
- package/dist/hcl/suppressions.d.ts.map +1 -1
- package/dist/hcl/suppressions.js +8 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -2
- package/dist/integrity.json +8 -5
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-catalog.js +27 -3
- package/dist/lint/audit-lineage.d.ts +14 -2
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/index.js +4 -0
- package/dist/lint/post-synth/record-store.d.ts +54 -0
- package/dist/lint/post-synth/record-store.d.ts.map +1 -0
- package/dist/lint/post-synth/record-store.js +104 -0
- package/dist/lint/post-synth/tf001.d.ts +2 -2
- package/dist/lint/post-synth/tf001.js +2 -2
- package/dist/lint/post-synth/tf024.d.ts +35 -11
- package/dist/lint/post-synth/tf024.d.ts.map +1 -1
- package/dist/lint/post-synth/tf024.js +92 -20
- package/dist/lint/post-synth/tf046.d.ts +64 -0
- package/dist/lint/post-synth/tf046.d.ts.map +1 -0
- package/dist/lint/post-synth/tf046.js +175 -0
- package/dist/lint/post-synth/tf047.d.ts +36 -0
- package/dist/lint/post-synth/tf047.d.ts.map +1 -0
- package/dist/lint/post-synth/tf047.js +94 -0
- package/dist/live-plan-reasons.d.ts +21 -0
- package/dist/live-plan-reasons.d.ts.map +1 -0
- package/dist/live-plan-reasons.js +47 -0
- package/dist/lock-rows.d.ts +36 -0
- package/dist/lock-rows.d.ts.map +1 -0
- package/dist/lock-rows.js +60 -0
- package/dist/lock-timeout.d.ts +22 -0
- package/dist/lock-timeout.d.ts.map +1 -0
- package/dist/lock-timeout.js +32 -0
- package/dist/lsp/option-keys.d.ts.map +1 -1
- package/dist/lsp/option-keys.js +2 -0
- package/dist/manifest.json +1 -1
- package/dist/module-catalog/index.d.ts +69 -0
- package/dist/module-catalog/index.d.ts.map +1 -0
- package/dist/module-catalog/index.js +110 -0
- package/dist/module-catalog/refresh-cli.d.ts +4 -0
- package/dist/module-catalog/refresh-cli.d.ts.map +1 -0
- package/dist/module-catalog/refresh-cli.js +64 -0
- package/dist/module-catalog/static-tier.d.ts +6 -0
- package/dist/module-catalog/static-tier.d.ts.map +1 -0
- package/dist/module-catalog/static-tier.js +1650 -0
- package/dist/module-catalog/types.d.ts +65 -0
- package/dist/module-catalog/types.d.ts.map +1 -0
- package/dist/module-catalog/types.js +33 -0
- package/dist/okf/index.md +3 -1
- package/dist/okf/rules/TF024.md +2 -2
- package/dist/okf/rules/TF046.md +11 -0
- package/dist/okf/rules/TF047.md +11 -0
- package/dist/op/activities/index.d.ts +6 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +5 -1
- package/dist/op/activities/live-detect.d.ts +4 -3
- package/dist/op/activities/live-detect.d.ts.map +1 -1
- package/dist/op/activities/live-detect.js +57 -32
- package/dist/op/activities/pin-rollout.d.ts +2 -0
- package/dist/op/activities/pin-rollout.d.ts.map +1 -1
- package/dist/op/activities/pin-rollout.js +1 -0
- package/dist/op/activities/terraform.d.ts +182 -5
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activities/terraform.js +224 -24
- package/dist/op/activity-contracts.d.ts +27 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/op/activity-contracts.js +19 -1
- package/dist/op/builders.d.ts +7 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +9 -0
- package/dist/pin/consumers.d.ts +35 -0
- package/dist/pin/consumers.d.ts.map +1 -0
- package/dist/pin/consumers.js +70 -0
- package/dist/pin/edit.d.ts.map +1 -1
- package/dist/pin/edit.js +35 -0
- package/dist/pin/rollout.d.ts +2 -0
- package/dist/pin/rollout.d.ts.map +1 -1
- package/dist/pin/rollout.js +14 -11
- package/dist/plan-shape.d.ts +30 -0
- package/dist/plan-shape.d.ts.map +1 -0
- package/dist/plan-shape.js +125 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +37 -18
- package/dist/registry/address.d.ts +63 -0
- package/dist/registry/address.d.ts.map +1 -0
- package/dist/registry/address.js +65 -0
- package/dist/registry/catalog-read.d.ts +14 -0
- package/dist/registry/catalog-read.d.ts.map +1 -0
- package/dist/registry/catalog-read.js +58 -0
- package/dist/registry/deprecated.d.ts +37 -0
- package/dist/registry/deprecated.d.ts.map +1 -0
- package/dist/registry/deprecated.js +43 -0
- package/dist/registry/docs.d.ts +96 -0
- package/dist/registry/docs.d.ts.map +1 -0
- package/dist/registry/docs.js +210 -0
- package/dist/registry/edge/edge.d.ts +46 -0
- package/dist/registry/edge/edge.d.ts.map +1 -0
- package/dist/registry/edge/edge.js +194 -0
- package/dist/registry/edge/index.d.ts +11 -0
- package/dist/registry/edge/index.d.ts.map +1 -0
- package/dist/registry/edge/index.js +10 -0
- package/dist/registry/edge/lambda-edge.d.ts +76 -0
- package/dist/registry/edge/lambda-edge.d.ts.map +1 -0
- package/dist/registry/edge/lambda-edge.js +95 -0
- package/dist/registry/edge/sigv4-web.d.ts +33 -0
- package/dist/registry/edge/sigv4-web.d.ts.map +1 -0
- package/dist/registry/edge/sigv4-web.js +56 -0
- package/dist/registry/edge/token-source.d.ts +73 -0
- package/dist/registry/edge/token-source.d.ts.map +1 -0
- package/dist/registry/edge/token-source.js +115 -0
- package/dist/registry/edge/worker.d.ts +45 -0
- package/dist/registry/edge/worker.d.ts.map +1 -0
- package/dist/registry/edge/worker.js +26 -0
- package/dist/registry/handler.d.ts +25 -0
- package/dist/registry/handler.d.ts.map +1 -0
- package/dist/registry/handler.js +75 -0
- package/dist/registry/headers-file.d.ts +21 -0
- package/dist/registry/headers-file.d.ts.map +1 -0
- package/dist/registry/headers-file.js +57 -0
- package/dist/registry/index.d.ts +31 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +30 -0
- package/dist/registry/mirror-paths.d.ts +9 -0
- package/dist/registry/mirror-paths.d.ts.map +1 -0
- package/dist/registry/mirror-paths.js +8 -0
- package/dist/registry/mirror.d.ts +130 -0
- package/dist/registry/mirror.d.ts.map +1 -0
- package/dist/registry/mirror.js +319 -0
- package/dist/registry/proxies.d.ts +73 -0
- package/dist/registry/proxies.d.ts.map +1 -0
- package/dist/registry/proxies.js +111 -0
- package/dist/registry/reader.d.ts +57 -0
- package/dist/registry/reader.d.ts.map +1 -0
- package/dist/registry/reader.js +198 -0
- package/dist/registry/semver.d.ts +16 -0
- package/dist/registry/semver.d.ts.map +1 -0
- package/dist/registry/semver.js +24 -0
- package/dist/registry/serve.d.ts +24 -0
- package/dist/registry/serve.d.ts.map +1 -0
- package/dist/registry/serve.js +64 -0
- package/dist/registry/source-forms.d.ts +79 -0
- package/dist/registry/source-forms.d.ts.map +1 -0
- package/dist/registry/source-forms.js +202 -0
- package/dist/registry/targets/artifactory.d.ts +28 -0
- package/dist/registry/targets/artifactory.d.ts.map +1 -0
- package/dist/registry/targets/artifactory.js +206 -0
- package/dist/registry/targets/fake-artifactory.d.ts +30 -0
- package/dist/registry/targets/fake-artifactory.d.ts.map +1 -0
- package/dist/registry/targets/fake-artifactory.js +142 -0
- package/dist/registry/targets/fake-gitlab.d.ts +39 -0
- package/dist/registry/targets/fake-gitlab.d.ts.map +1 -0
- package/dist/registry/targets/fake-gitlab.js +136 -0
- package/dist/registry/targets/fake-hcp.d.ts +50 -0
- package/dist/registry/targets/fake-hcp.d.ts.map +1 -0
- package/dist/registry/targets/fake-hcp.js +202 -0
- package/dist/registry/targets/fake-http.d.ts +31 -0
- package/dist/registry/targets/fake-http.d.ts.map +1 -0
- package/dist/registry/targets/fake-http.js +87 -0
- package/dist/registry/targets/fake-nexus.d.ts +27 -0
- package/dist/registry/targets/fake-nexus.d.ts.map +1 -0
- package/dist/registry/targets/fake-nexus.js +137 -0
- package/dist/registry/targets/gitlab.d.ts +53 -0
- package/dist/registry/targets/gitlab.d.ts.map +1 -0
- package/dist/registry/targets/gitlab.js +187 -0
- package/dist/registry/targets/hcp.d.ts +29 -0
- package/dist/registry/targets/hcp.d.ts.map +1 -0
- package/dist/registry/targets/hcp.js +310 -0
- package/dist/registry/targets/hosted.d.ts +62 -0
- package/dist/registry/targets/hosted.d.ts.map +1 -0
- package/dist/registry/targets/hosted.js +74 -0
- package/dist/registry/targets/index.d.ts +18 -0
- package/dist/registry/targets/index.d.ts.map +1 -0
- package/dist/registry/targets/index.js +17 -0
- package/dist/registry/targets/module-target.d.ts +68 -0
- package/dist/registry/targets/module-target.d.ts.map +1 -0
- package/dist/registry/targets/module-target.js +63 -0
- package/dist/registry/targets/nexus.d.ts +43 -0
- package/dist/registry/targets/nexus.d.ts.map +1 -0
- package/dist/registry/targets/nexus.js +250 -0
- package/dist/registry/targets/zip.d.ts +55 -0
- package/dist/registry/targets/zip.d.ts.map +1 -0
- package/dist/registry/targets/zip.js +278 -0
- package/dist/registry/writer.d.ts +101 -0
- package/dist/registry/writer.d.ts.map +1 -0
- package/dist/registry/writer.js +109 -0
- package/dist/report.d.ts +15 -6
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +28 -55
- package/dist/rules/record-store.ts +122 -0
- package/dist/rules/tf001.ts +2 -2
- package/dist/rules/tf024.ts +105 -22
- package/dist/rules/tf046.ts +204 -0
- package/dist/rules/tf047.ts +114 -0
- package/dist/skills/chant-terraform.md +1 -1
- package/dist/state/azure-blob.d.ts +9 -0
- package/dist/state/azure-blob.d.ts.map +1 -0
- package/dist/state/azure-blob.js +8 -0
- package/dist/state/backend.d.ts +158 -0
- package/dist/state/backend.d.ts.map +1 -0
- package/dist/state/backend.js +406 -0
- package/dist/state/exec.d.ts +39 -0
- package/dist/state/exec.d.ts.map +1 -0
- package/dist/state/exec.js +80 -0
- package/dist/state/gcs.d.ts +8 -0
- package/dist/state/gcs.d.ts.map +1 -0
- package/dist/state/gcs.js +7 -0
- package/dist/state/index.d.ts +15 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +14 -0
- package/dist/state/locks.d.ts +105 -0
- package/dist/state/locks.d.ts.map +1 -0
- package/dist/state/locks.js +130 -0
- package/dist/state/object-store.d.ts +8 -0
- package/dist/state/object-store.d.ts.map +1 -0
- package/dist/state/object-store.js +7 -0
- package/dist/state/root-states.d.ts +131 -0
- package/dist/state/root-states.d.ts.map +1 -0
- package/dist/state/root-states.js +241 -0
- package/dist/state/s3.d.ts +21 -0
- package/dist/state/s3.d.ts.map +1 -0
- package/dist/state/s3.js +19 -0
- package/dist/state/store.d.ts +51 -0
- package/dist/state/store.d.ts.map +1 -0
- package/dist/state/store.js +185 -0
- package/dist/state-ops.d.ts +255 -0
- package/dist/state-ops.d.ts.map +1 -0
- package/dist/state-ops.js +305 -0
- package/dist/state-reads.d.ts +57 -0
- package/dist/state-reads.d.ts.map +1 -0
- package/dist/state-reads.js +104 -0
- package/dist/terragrunt/affected.d.ts.map +1 -1
- package/dist/terragrunt/affected.js +2 -1
- package/dist/used-sources.d.ts +54 -0
- package/dist/used-sources.d.ts.map +1 -0
- package/dist/used-sources.js +157 -0
- package/package.json +24 -4
- package/src/__fixtures__/ACCEPTANCE.md +69 -7
- package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
- package/src/__fixtures__/compose/state-app.json +66 -0
- package/src/__fixtures__/compose/state-edge.json +40 -0
- package/src/__fixtures__/compose/state-net.json +58 -0
- package/src/__fixtures__/compose/three-app.plan.json +184 -0
- package/src/__fixtures__/compose/three-composed.plan.json +509 -0
- package/src/__fixtures__/compose/three-edge.plan.json +144 -0
- package/src/__fixtures__/compose/three-net.plan.json +174 -0
- package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
- package/src/__fixtures__/compose/working-after-apply.json +145 -0
- package/src/__fixtures__/config-blocks/app/main.tf +61 -0
- package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
- package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
- package/src/__fixtures__/config-files/shadow/main.tf +3 -0
- package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
- package/src/__fixtures__/config-files/shadow/other.tf +3 -0
- package/src/__fixtures__/config-files/tf/main.tf +11 -0
- package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
- package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
- package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
- package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
- package/src/__fixtures__/drift-record/README.md +16 -0
- package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
- package/src/__fixtures__/drift-rows/live-root-plan.json +70 -0
- package/src/__fixtures__/drift-rows/refresh-plan.json +61 -0
- package/src/__fixtures__/instance-forms/app/main.tf +110 -0
- package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
- package/src/__fixtures__/instance-vars/app/main.tf +52 -0
- package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
- package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
- package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
- package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
- package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
- package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
- package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
- package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
- package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
- package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
- package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
- package/src/__fixtures__/pr-loop/README.md +50 -0
- package/src/__fixtures__/pr-loop/chant.config.json +13 -0
- package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
- package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
- package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
- package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
- package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
- package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
- package/src/__fixtures__/report/applied.show.json +51 -0
- package/src/__fixtures__/report/generate.ts +63 -0
- package/src/__fixtures__/report/plan.show.json +200 -0
- package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
- package/src/__fixtures__/report/tf-apply/report.json +338 -0
- package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
- package/src/__fixtures__/report/tf-plan/report.json +320 -0
- package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
- package/src/__fixtures__/used-sources/root/extra.tofu +1 -0
- package/src/__fixtures__/used-sources/root/main.tf +31 -0
- package/src/__fixtures__/used-sources/root/modules/net/main.tf +14 -0
- package/src/__fixtures__/used-sources/root/modules/net/versions.tf.json +7 -0
- package/src/__fixtures__/used-sources/unit/terragrunt.hcl +3 -0
- package/src/applied-state.ts +52 -0
- package/src/backend-config.test.ts +105 -0
- package/src/backend-config.ts +133 -0
- package/src/behaviour/kinds.ts +4 -0
- package/src/behaviour/predict.ts +5 -2
- package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
- package/src/behaviour/reference-catalog.ts +16 -5
- package/src/behaviour/request.ts +3 -2
- package/src/change-set-instances.test.ts +98 -0
- package/src/changed-roots.ts +14 -16
- package/src/ci-setup.ts +42 -0
- package/src/commands.ts +4 -3
- package/src/components/capability-plugin.test.ts +198 -19
- package/src/components/capability-plugin.ts +383 -35
- package/src/components/change-set-graph.e2e.test.ts +11 -5
- package/src/components/compose-group.test.ts +486 -0
- package/src/components/compose-group.ts +489 -0
- package/src/components/demo-composed-roots.e2e.test.ts +440 -0
- package/src/components/member-outputs.test.ts +76 -0
- package/src/components/placeholder-preview.test.ts +147 -0
- package/src/components/placeholder-preview.ts +257 -0
- package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
- package/src/components/pr-loop.tofu.test.ts +113 -57
- package/src/components/release-inputs.ts +69 -0
- package/src/components/trace-facts.test.ts +21 -0
- package/src/components/trace-facts.ts +27 -0
- package/src/compose/apply-events.test.ts +62 -0
- package/src/compose/apply-events.ts +100 -0
- package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
- package/src/compose/estate.test.ts +421 -0
- package/src/compose/estate.ts +443 -0
- package/src/compose/hcl.ts +311 -0
- package/src/compose/module-address.ts +49 -0
- package/src/compose/root.test.ts +416 -0
- package/src/compose/root.ts +592 -0
- package/src/compose/run.test.ts +440 -0
- package/src/compose/run.tofu.test.ts +176 -0
- package/src/compose/run.ts +527 -0
- package/src/compose/slice.test.ts +184 -0
- package/src/compose/slice.ts +290 -0
- package/src/compose/state-address.ts +157 -0
- package/src/compose/state-merge.test.ts +131 -0
- package/src/compose/state-merge.ts +148 -0
- package/src/compose.test.ts +248 -0
- package/src/compose.ts +627 -0
- package/src/composites/terraform-apply-op.test.ts +9 -0
- package/src/composites/terraform-apply-op.ts +7 -3
- package/src/cone.ts +138 -0
- package/src/config-file-listers.test.ts +265 -0
- package/src/config.ts +44 -9
- package/src/cost-hold.ts +29 -0
- package/src/describe-resources.engine-facts.test.ts +411 -0
- package/src/describe-resources.live-module-keyed.test.ts +290 -0
- package/src/describe-resources.ts +416 -70
- package/src/drift-read.ts +284 -0
- package/src/drift-record.test.ts +223 -0
- package/src/drift-record.ts +455 -0
- package/src/drift-rows.test.ts +135 -0
- package/src/drift-rows.ts +137 -0
- package/src/exit-codes.test.ts +23 -0
- package/src/exit-codes.ts +59 -0
- package/src/hcl/config-blocks.test.ts +174 -0
- package/src/hcl/edges.ts +21 -1
- package/src/hcl/files.ts +234 -0
- package/src/hcl/instance-forms.test.ts +94 -0
- package/src/hcl/instances.test.ts +137 -0
- package/src/hcl/instances.ts +422 -0
- package/src/hcl/parse.test.ts +139 -1
- package/src/hcl/parse.ts +149 -16
- package/src/hcl/roots.ts +6 -0
- package/src/hcl/suppressions.ts +8 -2
- package/src/index.ts +79 -0
- package/src/lint/audit-catalog.ts +29 -3
- package/src/lint/audit-lineage.ts +14 -2
- package/src/lint/post-synth/fixtures/TF024/live-with-record-store/main.tf +18 -0
- package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/estate.chdf.hcl +1 -0
- package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/main.tf +10 -0
- package/src/lint/post-synth/fixtures/TF046/azurerm-no-resource-group/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF046/backend-config-only/main.tf +14 -0
- package/src/lint/post-synth/fixtures/TF046/derived-s3/backend.tf +7 -0
- package/src/lint/post-synth/fixtures/TF046/derived-s3/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF046/explicit-local/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF046/explicit-s3-beside-local-backend/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF046/implied-local/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF046/sidecar-implied/estate.chdf.hcl +1 -0
- package/src/lint/post-synth/fixtures/TF046/sidecar-implied/main.tf +5 -0
- package/src/lint/post-synth/fixtures/TF047/clear/main.tf +12 -0
- package/src/lint/post-synth/fixtures/TF047/refused/main.tf +12 -0
- package/src/lint/post-synth/fixtures/TF047/unknown-module/main.tf +12 -0
- package/src/lint/post-synth/fixtures/TF047/unknown-version/main.tf +12 -0
- package/src/lint/post-synth/index.ts +4 -0
- package/src/lint/post-synth/record-store.ts +122 -0
- package/src/lint/post-synth/tf001.ts +2 -2
- package/src/lint/post-synth/tf024.test.ts +26 -5
- package/src/lint/post-synth/tf024.ts +105 -22
- package/src/lint/post-synth/tf046.test.ts +137 -0
- package/src/lint/post-synth/tf046.ts +204 -0
- package/src/lint/post-synth/tf047.test.ts +56 -0
- package/src/lint/post-synth/tf047.ts +114 -0
- package/src/live-plan-reasons.ts +54 -0
- package/src/lock-rows.test.ts +70 -0
- package/src/lock-rows.ts +82 -0
- package/src/lock-timeout.ts +34 -0
- package/src/lsp/completions.test.ts +1 -1
- package/src/lsp/option-keys.ts +2 -0
- package/src/module-catalog/index.test.ts +93 -0
- package/src/module-catalog/index.ts +159 -0
- package/src/module-catalog/refresh-cli.ts +68 -0
- package/src/module-catalog/static-tier.ts +1659 -0
- package/src/module-catalog/types.ts +88 -0
- package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
- package/src/op/activities/index.ts +9 -0
- package/src/op/activities/live-detect.ts +55 -26
- package/src/op/activities/pin-rollout.ts +3 -0
- package/src/op/activities/registry.test.ts +2 -1
- package/src/op/activities/terraform.test.ts +55 -0
- package/src/op/activities/terraform.ts +312 -27
- package/src/op/activity-contracts.test.ts +1 -0
- package/src/op/activity-contracts.ts +24 -1
- package/src/op/builders.ts +14 -0
- package/src/op-wave-step.test.ts +109 -0
- package/src/pin/__fixtures__/consumers/live/dev/main.tf +4 -0
- package/src/pin/__fixtures__/consumers/live/prod/main.tf +21 -0
- package/src/pin/__fixtures__/consumers/units/vpc/terragrunt.hcl +7 -0
- package/src/pin/bundle.test.ts +2 -0
- package/src/pin/consumers.test.ts +57 -0
- package/src/pin/consumers.ts +95 -0
- package/src/pin/edit.ts +30 -0
- package/src/pin/rollout.ts +16 -11
- package/src/plan-shape.test.ts +148 -0
- package/src/plan-shape.ts +128 -0
- package/src/plugin.ts +39 -19
- package/src/registry/__fixtures__/docs/documented/.terraform-docs.yml +3 -0
- package/src/registry/__fixtures__/docs/documented/README.md +1 -0
- package/src/registry/__fixtures__/docs/documented/main.tf +8 -0
- package/src/registry/__fixtures__/docs/json/main.tf.json +14 -0
- package/src/registry/__fixtures__/docs/vpc/README.md +3 -0
- package/src/registry/__fixtures__/docs/vpc/examples/complete/main.tf +5 -0
- package/src/registry/__fixtures__/docs/vpc/examples/simple/README.md +1 -0
- package/src/registry/__fixtures__/docs/vpc/examples/simple/main.tf +4 -0
- package/src/registry/__fixtures__/docs/vpc/main.tf +14 -0
- package/src/registry/__fixtures__/docs/vpc/outputs.tf +9 -0
- package/src/registry/__fixtures__/docs/vpc/variables.tf +16 -0
- package/src/registry/__fixtures__/proxied-root/main.tf +12 -0
- package/src/registry/address.ts +100 -0
- package/src/registry/catalog-read.ts +56 -0
- package/src/registry/deprecated.test.ts +139 -0
- package/src/registry/deprecated.ts +76 -0
- package/src/registry/docs.test.ts +140 -0
- package/src/registry/docs.ts +299 -0
- package/src/registry/edge/bundle.test.ts +50 -0
- package/src/registry/edge/edge.test.ts +176 -0
- package/src/registry/edge/edge.ts +211 -0
- package/src/registry/edge/index.ts +10 -0
- package/src/registry/edge/lambda-edge.ts +127 -0
- package/src/registry/edge/sigv4-web.ts +79 -0
- package/src/registry/edge/token-source.test.ts +258 -0
- package/src/registry/edge/token-source.ts +169 -0
- package/src/registry/edge/worker.ts +61 -0
- package/src/registry/handler.ts +89 -0
- package/src/registry/headers-file.ts +61 -0
- package/src/registry/index.ts +31 -0
- package/src/registry/mirror-paths.ts +10 -0
- package/src/registry/mirror.test.ts +198 -0
- package/src/registry/mirror.ts +395 -0
- package/src/registry/proxies.test.ts +153 -0
- package/src/registry/proxies.ts +132 -0
- package/src/registry/reader.ts +225 -0
- package/src/registry/registry.test.ts +262 -0
- package/src/registry/semver.ts +34 -0
- package/src/registry/serve.ts +82 -0
- package/src/registry/source-forms.test.ts +87 -0
- package/src/registry/source-forms.ts +216 -0
- package/src/registry/targets/artifactory.test.ts +248 -0
- package/src/registry/targets/artifactory.ts +257 -0
- package/src/registry/targets/fake-artifactory.ts +158 -0
- package/src/registry/targets/fake-gitlab.ts +161 -0
- package/src/registry/targets/fake-hcp.ts +233 -0
- package/src/registry/targets/fake-http.ts +97 -0
- package/src/registry/targets/fake-nexus.ts +152 -0
- package/src/registry/targets/gitlab.test.ts +208 -0
- package/src/registry/targets/gitlab.ts +223 -0
- package/src/registry/targets/hcp.test.ts +230 -0
- package/src/registry/targets/hcp.ts +360 -0
- package/src/registry/targets/hosted.ts +117 -0
- package/src/registry/targets/index.ts +17 -0
- package/src/registry/targets/module-target.ts +131 -0
- package/src/registry/targets/nexus.test.ts +220 -0
- package/src/registry/targets/nexus.ts +314 -0
- package/src/registry/targets/zip.test.ts +65 -0
- package/src/registry/targets/zip.ts +287 -0
- package/src/registry/writer.ts +155 -0
- package/src/report.test.ts +82 -1
- package/src/report.ts +31 -54
- package/src/signal-only-root.test.ts +72 -0
- package/src/skills/chant-terraform.md +1 -1
- package/src/state/azure-blob.ts +8 -0
- package/src/state/backend-verdict.test.ts +187 -0
- package/src/state/backend.test.ts +127 -0
- package/src/state/backend.ts +488 -0
- package/src/state/exec.ts +111 -0
- package/src/state/gcs.ts +7 -0
- package/src/state/index.ts +15 -0
- package/src/state/locks.test.ts +84 -0
- package/src/state/locks.ts +201 -0
- package/src/state/object-store.ts +7 -0
- package/src/state/root-states-floci.e2e.test.ts +66 -0
- package/src/state/root-states.test.ts +462 -0
- package/src/state/root-states.ts +309 -0
- package/src/state/s3.ts +21 -0
- package/src/state/store.ts +225 -0
- package/src/state-ops.test.ts +182 -0
- package/src/state-ops.ts +462 -0
- package/src/state-reads.ts +116 -0
- package/src/terragrunt/affected.ts +2 -1
- package/src/used-sources.test.ts +75 -0
- package/src/used-sources.ts +204 -0
- package/src/workspace-kinds.test.ts +4 -17
- package/workspace-kinds.json +1 -24
- package/dist/workspace-member.d.ts +0 -30
- package/dist/workspace-member.d.ts.map +0 -1
- package/dist/workspace-member.js +0 -91
- package/schemas/pulumi.schema.json +0 -34
- package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
- package/src/testdata/pulumi-member/stack-export.json +0 -53
- package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
- package/src/workspace-member.test.ts +0 -89
- package/src/workspace-member.ts +0 -126
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift rows (#4580): one row per object a plan found drifted, by address,
|
|
3
|
+
* with its action and the object's real name, counted the way a product's
|
|
4
|
+
* drift report counts. `./drift-record.ts` answers a different question (the
|
|
5
|
+
* status of each instance the plan names), so it skips create rows, carries
|
|
6
|
+
* no names and stays as it is.
|
|
7
|
+
*
|
|
8
|
+
* A stock root's refresh-only plan reports drift as `resource_drift`, and
|
|
9
|
+
* every entry there is one row, data blocks and creates included. A full
|
|
10
|
+
* plan of a root under live resource markers (`live: true`) reads drift from
|
|
11
|
+
* `resource_changes` instead, seen from the other side: a create there is an
|
|
12
|
+
* object that no longer exists (delete), a delete one that exists outside
|
|
13
|
+
* the code (create), a replacement or any other pair of actions an update.
|
|
14
|
+
* Data blocks, no-op and read entries are not drift.
|
|
15
|
+
*
|
|
16
|
+
* These are terragucci's rules (packages/terragucci/src/report/drift.ts:
|
|
17
|
+
* `liveDrift`, `driftCount`, `driftNames`), so a row count here equals the
|
|
18
|
+
* count its report gives for the same plan.
|
|
19
|
+
*
|
|
20
|
+
* A row carries no attribute values. Its `name` is the first non-empty
|
|
21
|
+
* string among `name`, `id`, `bucket`, `function_name` and `arn`, read from
|
|
22
|
+
* the object as found and then as the code has it; a deleted object has no
|
|
23
|
+
* other place its name survives.
|
|
24
|
+
*
|
|
25
|
+
* The module imports nothing, so a product can load it behind a
|
|
26
|
+
* feature-detect without pulling the lexicon's entry point.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
type Json = Record<string, unknown>;
|
|
30
|
+
|
|
31
|
+
const isObject = (v: unknown): v is Json => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
32
|
+
|
|
33
|
+
/** One drifted object. */
|
|
34
|
+
export interface TerraformDriftRow {
|
|
35
|
+
/** The resource address, `""` when the plan's entry has none. */
|
|
36
|
+
address: string;
|
|
37
|
+
/**
|
|
38
|
+
* What happened outside the code: `update`, `delete` (the object is gone),
|
|
39
|
+
* `create` (an object exists that state or code does not hold), or on a
|
|
40
|
+
* stock root whatever single action the plan's entry names. On a stock
|
|
41
|
+
* root a delete-and-create pair is `replace`, and any other pair `update`.
|
|
42
|
+
*/
|
|
43
|
+
action: string;
|
|
44
|
+
/** The resource type, when the entry names one. */
|
|
45
|
+
type?: string;
|
|
46
|
+
/** The object's real name, by the rule in this module's header. */
|
|
47
|
+
name?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TerraformPlanDriftRowsOptions {
|
|
51
|
+
/**
|
|
52
|
+
* The plan is a full plan of a root under live resource markers, whose
|
|
53
|
+
* changes are its drift. Default false: a refresh-only plan's
|
|
54
|
+
* `resource_drift`.
|
|
55
|
+
*/
|
|
56
|
+
live?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const FLIP: Record<string, string> = { create: "delete", delete: "create" };
|
|
60
|
+
const NAME_KEYS = ["name", "id", "bucket", "function_name", "arn"] as const;
|
|
61
|
+
|
|
62
|
+
function nameOf(values: readonly unknown[]): string | undefined {
|
|
63
|
+
const objects = values.filter(isObject);
|
|
64
|
+
for (const key of NAME_KEYS) {
|
|
65
|
+
const v = objects.map((x) => x[key]).find((x) => typeof x === "string" && x !== "");
|
|
66
|
+
if (typeof v === "string") return v;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function rowOf(entry: unknown, action: string, values: readonly unknown[]): TerraformDriftRow {
|
|
72
|
+
const r = isObject(entry) ? entry : {};
|
|
73
|
+
const name = nameOf(values);
|
|
74
|
+
return {
|
|
75
|
+
address: typeof r.address === "string" ? r.address : "",
|
|
76
|
+
action,
|
|
77
|
+
...(typeof r.type === "string" && r.type !== "" ? { type: r.type } : {}),
|
|
78
|
+
...(name !== undefined ? { name } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function stockAction(actions: readonly string[]): string {
|
|
83
|
+
if (actions.length === 1) return actions[0]!;
|
|
84
|
+
if (actions.length === 2 && actions.includes("delete") && actions.includes("create")) return "replace";
|
|
85
|
+
return "update";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A refresh-only plan's `resource_drift`, one row per entry. */
|
|
89
|
+
function stockRows(doc: Json): TerraformDriftRow[] {
|
|
90
|
+
const list = Array.isArray(doc.resource_drift) ? doc.resource_drift : [];
|
|
91
|
+
return list.map((entry) => {
|
|
92
|
+
const ch = isObject(entry) && isObject(entry.change) ? entry.change : {};
|
|
93
|
+
const actions = Array.isArray(ch.actions) ? ch.actions.map(String) : [];
|
|
94
|
+
return rowOf(entry, stockAction(actions), [ch.after, ch.before]);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A live root's full plan, its changes read as drift. */
|
|
99
|
+
function liveRows(doc: Json): TerraformDriftRow[] {
|
|
100
|
+
const list = Array.isArray(doc.resource_changes) ? doc.resource_changes : [];
|
|
101
|
+
const out: TerraformDriftRow[] = [];
|
|
102
|
+
for (const entry of list) {
|
|
103
|
+
if (!isObject(entry) || entry.mode === "data") continue;
|
|
104
|
+
const ch = isObject(entry.change) ? entry.change : {};
|
|
105
|
+
const actions = Array.isArray(ch.actions) ? ch.actions.map(String) : [];
|
|
106
|
+
if (actions.length === 0 || actions.every((a) => a === "no-op" || a === "read")) continue;
|
|
107
|
+
const action = actions.length === 1 ? (FLIP[actions[0]!] ?? actions[0]!) : "update";
|
|
108
|
+
const live = isObject(ch.before) ? ch.before : null;
|
|
109
|
+
let code = isObject(ch.after) ? ch.after : null;
|
|
110
|
+
// A value the plan knows only after the apply is not a difference: the live one stands for it.
|
|
111
|
+
const unknown = isObject(ch.after_unknown) ? ch.after_unknown : {};
|
|
112
|
+
if (code && live) code = { ...code, ...Object.fromEntries(Object.keys(unknown).filter((k) => unknown[k] === true && k in live).map((k) => [k, live[k]])) };
|
|
113
|
+
out.push(rowOf(entry, action, [live, code]));
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The drift a plan (`show -json`, parsed) found, one row per drifted object
|
|
120
|
+
* in the plan's order. Its length is the drift count.
|
|
121
|
+
*/
|
|
122
|
+
export function terraformPlanDriftRows(plan: unknown, options: TerraformPlanDriftRowsOptions = {}): TerraformDriftRow[] {
|
|
123
|
+
if (!isObject(plan)) return [];
|
|
124
|
+
return options.live ? liveRows(plan) : stockRows(plan);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** How many objects a plan found drifted: `terraformPlanDriftRows(plan, options).length`. */
|
|
128
|
+
export function terraformPlanDriftCount(plan: unknown, options: TerraformPlanDriftRowsOptions = {}): number {
|
|
129
|
+
return terraformPlanDriftRows(plan, options).length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Each drifted object's real name by address, for the rows that have one. */
|
|
133
|
+
export function terraformPlanDriftNames(plan: unknown, options: TerraformPlanDriftRowsOptions = {}): Map<string, string> {
|
|
134
|
+
const out = new Map<string, string>();
|
|
135
|
+
for (const row of terraformPlanDriftRows(plan, options)) if (row.name !== undefined) out.set(row.address, row.name);
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { CHANT_EXIT } from "@intentius/chant/exit-codes";
|
|
3
|
+
import { terraformExitOf, terraformPlanMoved } from "./exit-codes";
|
|
4
|
+
|
|
5
|
+
describe("terraform, tofu and choudoufu exit statuses on chant's table (#4553)", () => {
|
|
6
|
+
test("choudoufu's refusals of a moved plan are chant's moved code", () => {
|
|
7
|
+
expect(terraformExitOf({ command: "live-wave-apply", code: 3 })).toBe(CHANT_EXIT.moved);
|
|
8
|
+
expect(terraformExitOf({ command: "apply", code: 3, refusal: "approval-mismatch" })).toBe(CHANT_EXIT.moved);
|
|
9
|
+
expect(terraformPlanMoved({ command: "live-wave-apply", code: 3 })).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("a plan made against another estate is a failure, and so is a root that failed", () => {
|
|
13
|
+
expect(terraformExitOf({ command: "apply", code: 3, refusal: "wrong-estate" })).toBe(CHANT_EXIT.failed);
|
|
14
|
+
expect(terraformExitOf({ command: "live-wave-apply", code: 4 })).toBe(CHANT_EXIT.failed);
|
|
15
|
+
expect(terraformExitOf({ command: "live-plan-set", code: 4 })).toBe(CHANT_EXIT.failed);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("a plan's 2 is drift only under a drift check", () => {
|
|
19
|
+
expect(terraformExitOf({ command: "plan", code: 2 })).toBe(CHANT_EXIT.done);
|
|
20
|
+
expect(terraformExitOf({ command: "plan", code: 2, drift: true })).toBe(CHANT_EXIT.drift);
|
|
21
|
+
expect(terraformExitOf({ command: "plan", code: 1 })).toBe(CHANT_EXIT.failed);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTofu, Terraform and choudoufu outcomes mapped onto chant's exit-code
|
|
3
|
+
* table (#4553, `CHANT_EXIT` in `@intentius/chant/exit-codes`).
|
|
4
|
+
*
|
|
5
|
+
* The lexicon never picks the process's exit code itself: for a moved plan it
|
|
6
|
+
* throws an error core marks as moved (`planMoved: true`, read by
|
|
7
|
+
* `isPlanMovedError`), and core exits `CHANT_EXIT.moved`. This mapping says
|
|
8
|
+
* which engine outcome is which row of the table, so the throw sites and the
|
|
9
|
+
* reference page agree.
|
|
10
|
+
*
|
|
11
|
+
* | Engine outcome | Table |
|
|
12
|
+
* |---|---|
|
|
13
|
+
* | `plan -detailed-exitcode` 0 or 2 | not an exit: the plan's answer. 2 under a drift check is `drift` |
|
|
14
|
+
* | any 1 | `failed` |
|
|
15
|
+
* | choudoufu `apply <planfile>` 3, approval mismatch | `moved` |
|
|
16
|
+
* | choudoufu `apply <planfile>` 3, plan made against another estate | `failed`: the pipeline handed the wrong file, and no approval fixes that |
|
|
17
|
+
* | choudoufu `live-wave-apply` 3, set moved | `moved` |
|
|
18
|
+
* | choudoufu `live-wave-apply` 4 or `live-plan-set` 4, a root failed | `failed` |
|
|
19
|
+
* | a composed plan that moved | `moved` |
|
|
20
|
+
* | `live-affected` 2, `live-cone` 2 | planner answers, never a job's exit |
|
|
21
|
+
*/
|
|
22
|
+
import { CHANT_EXIT, type ChantExitCode } from "@intentius/chant/exit-codes";
|
|
23
|
+
import { CHOUDOUFU_APPROVAL_EXIT_CODE, CHOUDOUFU_WAVE_EXIT, type TerraformApprovalRefusal } from "./op/activities/terraform";
|
|
24
|
+
|
|
25
|
+
/** The command whose exit status is mapped. */
|
|
26
|
+
export type TerraformExitSource =
|
|
27
|
+
/** `terraform|tofu|choudoufu plan -detailed-exitcode`; `drift` says a drift check asked for it. */
|
|
28
|
+
| { command: "plan"; code: number; drift?: boolean }
|
|
29
|
+
/** `choudoufu apply <planfile>` on a live root; `refusal` is `classifyApprovalRefusal`'s answer for its output. */
|
|
30
|
+
| { command: "apply"; code: number; refusal?: TerraformApprovalRefusal }
|
|
31
|
+
/** `choudoufu live-wave-apply`. */
|
|
32
|
+
| { command: "live-wave-apply"; code: number }
|
|
33
|
+
/** `choudoufu live-plan-set`. */
|
|
34
|
+
| { command: "live-plan-set"; code: number };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The table's code for a terraform, tofu or choudoufu exit status. A plan's
|
|
38
|
+
* 0 and 2 are both `done` unless a drift check asked, when 2 is `drift`.
|
|
39
|
+
*/
|
|
40
|
+
export function terraformExitOf(source: TerraformExitSource): ChantExitCode {
|
|
41
|
+
const { code } = source;
|
|
42
|
+
if (code === 0) return CHANT_EXIT.done;
|
|
43
|
+
switch (source.command) {
|
|
44
|
+
case "plan":
|
|
45
|
+
if (code === 2) return source.drift ? CHANT_EXIT.drift : CHANT_EXIT.done;
|
|
46
|
+
return CHANT_EXIT.failed;
|
|
47
|
+
case "apply":
|
|
48
|
+
return code === CHOUDOUFU_APPROVAL_EXIT_CODE && source.refusal === "approval-mismatch" ? CHANT_EXIT.moved : CHANT_EXIT.failed;
|
|
49
|
+
case "live-wave-apply":
|
|
50
|
+
return code === CHOUDOUFU_WAVE_EXIT.setMoved ? CHANT_EXIT.moved : CHANT_EXIT.failed;
|
|
51
|
+
case "live-plan-set":
|
|
52
|
+
return code === 2 ? CHANT_EXIT.done : CHANT_EXIT.failed;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether `source` is a plan that moved since its approval: the outcomes the lexicon throws a `planMoved` error for. */
|
|
57
|
+
export function terraformPlanMoved(source: TerraformExitSource): boolean {
|
|
58
|
+
return terraformExitOf(source) === CHANT_EXIT.moved;
|
|
59
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `moved`, `import`, `removed` and `check` blocks as entities (chant #4458).
|
|
3
|
+
*
|
|
4
|
+
* The fixture is `src/__fixtures__/config-blocks/app/`: one root holding two
|
|
5
|
+
* `moved` blocks, an `import`, a `removed` and a `check` with a scoped `data`
|
|
6
|
+
* block, rendered through `renderTerraformRoots` and core's `buildGraphIr`,
|
|
7
|
+
* the pair `chant graph --format ir` runs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
13
|
+
import { buildGraphIr, type GraphIR, type IREdge } from "@intentius/chant/graph-ir";
|
|
14
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
15
|
+
import { renderTerraformRoots } from "./roots";
|
|
16
|
+
import {
|
|
17
|
+
CHECK_TYPE,
|
|
18
|
+
IMPORT_TYPE,
|
|
19
|
+
MOVED_TYPE,
|
|
20
|
+
REMOVED_TYPE,
|
|
21
|
+
blocksToEntities,
|
|
22
|
+
type TerraformEntity,
|
|
23
|
+
} from "./parse";
|
|
24
|
+
|
|
25
|
+
const TREE = join(dirname(fileURLToPath(import.meta.url)), "..", "__fixtures__", "config-blocks");
|
|
26
|
+
|
|
27
|
+
let entities: Map<string, Declarable>;
|
|
28
|
+
let warnings: string[];
|
|
29
|
+
let ir: GraphIR;
|
|
30
|
+
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
const rendered = await renderTerraformRoots({ projectRoot: TREE, roots: { app: { dir: "./app" } } });
|
|
33
|
+
entities = rendered.entities;
|
|
34
|
+
warnings = rendered.warnings;
|
|
35
|
+
ir = buildGraphIr(entities);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function entity(key: string): TerraformEntity {
|
|
39
|
+
const e = entities.get(key);
|
|
40
|
+
expect(e, key).toBeDefined();
|
|
41
|
+
return e as TerraformEntity;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function edgesFrom(from: string): Array<[string, string]> {
|
|
45
|
+
return ir.edges
|
|
46
|
+
.filter((e: IREdge) => e.from === from)
|
|
47
|
+
.map((e: IREdge) => [e.to, e.viaAttr ?? ""] as [string, string])
|
|
48
|
+
.sort();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("moved blocks", () => {
|
|
52
|
+
it("become Terraform::Moved entities", () => {
|
|
53
|
+
expect(entity("app/moved").entityType).toBe(MOVED_TYPE);
|
|
54
|
+
expect(entity("app/moved").props.address).toBe("moved");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("get distinct keys when a root holds two", () => {
|
|
58
|
+
expect(entity("app/moved~2").entityType).toBe(MOVED_TYPE);
|
|
59
|
+
expect([...entities.keys()].filter((k) => entities.get(k)!.entityType === MOVED_TYPE).sort()).toEqual([
|
|
60
|
+
"app/moved",
|
|
61
|
+
"app/moved~2",
|
|
62
|
+
]);
|
|
63
|
+
expect(entity("app/moved").props.line).not.toBe(entity("app/moved~2").props.line);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("have an edge to `to`, and to `from` when `from` is still declared", () => {
|
|
67
|
+
// `aws_instance.old` and `module.legacy` are not declared, so only `to` draws.
|
|
68
|
+
expect(edgesFrom("app/moved")).toEqual([["app/aws_instance.web", "to"]]);
|
|
69
|
+
expect(edgesFrom("app/moved~2")).toEqual([["app/module.network", "to"]]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("draws the `from` edge when the address it names is declared", async () => {
|
|
73
|
+
const parsed = await blocksToEntities(
|
|
74
|
+
[
|
|
75
|
+
{
|
|
76
|
+
name: "main.tf",
|
|
77
|
+
source: [
|
|
78
|
+
'resource "aws_instance" "a" {}',
|
|
79
|
+
'resource "aws_instance" "b" {}',
|
|
80
|
+
"moved {",
|
|
81
|
+
" from = aws_instance.a",
|
|
82
|
+
" to = aws_instance.b",
|
|
83
|
+
"}",
|
|
84
|
+
].join("\n"),
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
"app",
|
|
88
|
+
);
|
|
89
|
+
const { resolveEntityReferences } = await import("./edges");
|
|
90
|
+
await resolveEntityReferences(parsed);
|
|
91
|
+
const moved = parsed.get("app/moved") as TerraformEntity;
|
|
92
|
+
expect(moved.references).toEqual([
|
|
93
|
+
{ to: "app/aws_instance.a", viaAttr: "from" },
|
|
94
|
+
{ to: "app/aws_instance.b", viaAttr: "to" },
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("import blocks", () => {
|
|
100
|
+
it("become a Terraform::Import entity with an edge to `to`", () => {
|
|
101
|
+
expect(entity("app/import").entityType).toBe(IMPORT_TYPE);
|
|
102
|
+
expect(edgesFrom("app/import")).toEqual([["app/aws_lb.front", "to"]]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("removed blocks", () => {
|
|
107
|
+
it("become a Terraform::Removed entity", () => {
|
|
108
|
+
expect(entity("app/removed").entityType).toBe(REMOVED_TYPE);
|
|
109
|
+
expect(entity("app/removed").props.body["from"]).toBe("${aws_instance.gone}");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("draw a `from` edge only when the address is still declared", async () => {
|
|
113
|
+
expect(edgesFrom("app/removed")).toEqual([]);
|
|
114
|
+
const parsed = await blocksToEntities(
|
|
115
|
+
[{ name: "main.tf", source: 'resource "aws_instance" "a" {}\nremoved {\n from = aws_instance.a\n}\n' }],
|
|
116
|
+
"app",
|
|
117
|
+
);
|
|
118
|
+
const { resolveEntityReferences } = await import("./edges");
|
|
119
|
+
await resolveEntityReferences(parsed);
|
|
120
|
+
expect((parsed.get("app/removed") as TerraformEntity).references).toEqual([
|
|
121
|
+
{ to: "app/aws_instance.a", viaAttr: "from" },
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("check blocks", () => {
|
|
127
|
+
it("become a Terraform::Check entity keyed by its name", () => {
|
|
128
|
+
expect(entity("app/check.health").entityType).toBe(CHECK_TYPE);
|
|
129
|
+
expect(entity("app/check.health").props.address).toBe("check.health");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("have an edge per reference, including the scoped data block's", () => {
|
|
133
|
+
expect(edgesFrom("app/check.health")).toEqual([
|
|
134
|
+
["app/aws_lb.front", "data"],
|
|
135
|
+
["app/var.region", "assert"],
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("read the scoped data block, never the root-level one of the same address", () => {
|
|
140
|
+
expect(ir.edges.some((e) => e.from === "app/check.health" && e.to === "app/data.http.site")).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("a `from` naming an undeclared address", () => {
|
|
145
|
+
it("does not fail the build", () => {
|
|
146
|
+
expect(warnings).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("raises no dangling reference", () => {
|
|
150
|
+
expect(ir.dangling ?? []).toEqual([]);
|
|
151
|
+
for (const e of ir.edges) expect(entities.has(e.to)).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("the same blocks in Terraform's JSON syntax (chant#4457)", () => {
|
|
156
|
+
/** Each config block's entity as the acceptance compares it: type, address and body, by key. */
|
|
157
|
+
const configBlocks = (all: Map<string, Declarable>): Record<string, { type: string; address: string; body: unknown }> =>
|
|
158
|
+
Object.fromEntries(
|
|
159
|
+
[...all]
|
|
160
|
+
.filter(([, e]) => [MOVED_TYPE, IMPORT_TYPE, REMOVED_TYPE, CHECK_TYPE].includes(e.entityType))
|
|
161
|
+
.map(([key, e]) => {
|
|
162
|
+
const p = (e as TerraformEntity).props;
|
|
163
|
+
return [key, { type: e.entityType, address: p.address, body: p.body }];
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
it("main.tf.json's moved, import, removed and check blocks are the entities main.tf's are", async () => {
|
|
168
|
+
const json = (await renderTerraformRoots({ projectRoot: TREE, roots: { app: { dir: "./app-json" } } })).entities;
|
|
169
|
+
const fromJson = configBlocks(json);
|
|
170
|
+
expect(Object.keys(fromJson).sort()).toEqual(["app/check.health", "app/import", "app/moved", "app/moved~2", "app/removed"]);
|
|
171
|
+
expect(fromJson).toEqual(configBlocks(entities));
|
|
172
|
+
for (const key of Object.keys(fromJson)) expect((json.get(key) as TerraformEntity).props.file).toBe("main.tf.json");
|
|
173
|
+
});
|
|
174
|
+
});
|
package/src/hcl/edges.ts
CHANGED
|
@@ -88,7 +88,7 @@ import { isResourceDeclarable, type Declarable } from "@intentius/chant/declarab
|
|
|
88
88
|
import type { EntityReference } from "@intentius/chant/graph-ir";
|
|
89
89
|
import { loadHcl2json, type Hcl2Json } from "@intentius/chant/terraform/hcl2json";
|
|
90
90
|
import { refFromAccessor } from "@intentius/chant/terraform/graph";
|
|
91
|
-
import { LOCALS_TYPE, scopeOfKey, type BlockBody, type TerraformEntity } from "./parse";
|
|
91
|
+
import { CHECK_TYPE, LOCALS_TYPE, scopeOfKey, type BlockBody, type TerraformEntity } from "./parse";
|
|
92
92
|
|
|
93
93
|
/** An identifier as Terraform's grammar allows it. Same as `./references.ts`. */
|
|
94
94
|
const NAME = "[A-Za-z_][A-Za-z0-9_-]*";
|
|
@@ -244,8 +244,12 @@ export function referencesOfEntity(
|
|
|
244
244
|
const index = scopes.get(scopeOfKey(key));
|
|
245
245
|
if (!index) return [];
|
|
246
246
|
|
|
247
|
+
const shadowed = entity.entityType === CHECK_TYPE ? scopedDataAddresses(bodyOf(entity)) : undefined;
|
|
247
248
|
const grouped = new Map<string, { to: string; viaAttr: string; attrs: Set<string> }>();
|
|
248
249
|
for (const raw of rawReferences(bodyOf(entity), accessors)) {
|
|
250
|
+
// An assertion reading the check's own scoped `data` block reads that
|
|
251
|
+
// block, never a root-level data source of the same address (#4458).
|
|
252
|
+
if (shadowed?.has(raw.address)) continue;
|
|
249
253
|
const to = resolveAddress(raw.address, index);
|
|
250
254
|
if (!to || to === key) continue;
|
|
251
255
|
const groupKey = `${to}${raw.via}`;
|
|
@@ -270,6 +274,22 @@ export function referencesOfEntity(
|
|
|
270
274
|
return out.sort((a, b) => cmp(a.to, b.to) || cmp(a.viaAttr ?? "", b.viaAttr ?? ""));
|
|
271
275
|
}
|
|
272
276
|
|
|
277
|
+
/**
|
|
278
|
+
* The `data.<type>.<name>` addresses a `check` block declares in its own
|
|
279
|
+
* scope (#4458). hcl2json gives them as `data: { <type>: { <name>: [...] } }`
|
|
280
|
+
* inside the check's body.
|
|
281
|
+
*/
|
|
282
|
+
function scopedDataAddresses(body: BlockBody): Set<string> {
|
|
283
|
+
const out = new Set<string>();
|
|
284
|
+
const data = body["data"];
|
|
285
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return out;
|
|
286
|
+
for (const [type, named] of Object.entries(data as Record<string, unknown>)) {
|
|
287
|
+
if (typeof named !== "object" || named === null || Array.isArray(named)) continue;
|
|
288
|
+
for (const name of Object.keys(named as Record<string, unknown>)) out.add(`data.${type}.${name}`);
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
273
293
|
/** Code-point ordering, so output does not depend on the machine's locale. */
|
|
274
294
|
function cmp(a: string, b: string): number {
|
|
275
295
|
return a < b ? -1 : a > b ? 1 : 0;
|
package/src/hcl/files.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which files of a directory are a root's configuration, and how a JSON-syntax
|
|
3
|
+
* one reads (chant#4457). Every lister in the lexicon asks here, so the parser,
|
|
4
|
+
* change detection, the backend readers, the report and the rollout pin all
|
|
5
|
+
* see the same file set for one root.
|
|
6
|
+
*
|
|
7
|
+
* The set follows the root's binary. Terraform reads `.tf` and `.tf.json`.
|
|
8
|
+
* OpenTofu (and choudoufu, which is OpenTofu) also reads `.tofu` and
|
|
9
|
+
* `.tofu.json`, and when `x.tofu` and `x.tf` both exist it reads `x.tofu`
|
|
10
|
+
* only (likewise `x.tofu.json` over `x.tf.json`). A binary that is not given
|
|
11
|
+
* is `terraform`, the config's default.
|
|
12
|
+
*
|
|
13
|
+
* `parse.ts` re-exports all of this; the module is separate so the readers
|
|
14
|
+
* that never parse HCL (`state/backend.ts`, `changed-roots.ts`) do not load
|
|
15
|
+
* the parser's dependencies.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readdirSync, statSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
/** The configuration file suffixes Terraform reads. */
|
|
22
|
+
export const TERRAFORM_SUFFIXES: readonly string[] = [".tf", ".tf.json"];
|
|
23
|
+
/** The ones OpenTofu reads as well. */
|
|
24
|
+
export const TOFU_SUFFIXES: readonly string[] = [".tofu", ".tofu.json"];
|
|
25
|
+
|
|
26
|
+
/** Whether a root run by `binary` reads `.tofu` files: `tofu` and `choudoufu` do (by name or path), `terraform` and an unset binary do not. */
|
|
27
|
+
export function readsTofuFiles(binary?: string): boolean {
|
|
28
|
+
const base = (binary ?? "terraform").split(/[\\/]/).pop() ?? "";
|
|
29
|
+
return base.startsWith("tofu") || base.startsWith("choudoufu");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Whether `name` is a configuration file in Terraform's JSON syntax (`.tf.json`, `.tofu.json`). */
|
|
33
|
+
export function isJsonConfigFile(name: string): boolean {
|
|
34
|
+
return name.endsWith(".tf.json") || name.endsWith(".tofu.json");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Whether `name` is a `.tofu` or `.tofu.json` file. */
|
|
38
|
+
export function isTofuConfigFile(name: string): boolean {
|
|
39
|
+
return TOFU_SUFFIXES.some((s) => name.endsWith(s));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Whether `name` is a configuration file a root run by `binary` reads, before the `.tofu`-over-`.tf` rule. */
|
|
43
|
+
export function isConfigFileFor(name: string, binary?: string): boolean {
|
|
44
|
+
if (TERRAFORM_SUFFIXES.some((s) => name.endsWith(s))) return true;
|
|
45
|
+
return readsTofuFiles(binary) && isTofuConfigFile(name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The configuration files among `names` a root run by `binary` reads, in
|
|
50
|
+
* filename order. For a tofu or choudoufu root, `x.tf` is dropped when
|
|
51
|
+
* `x.tofu` is present and `x.tf.json` when `x.tofu.json` is, which is
|
|
52
|
+
* OpenTofu's own rule. Names may carry a directory; the rule compares names
|
|
53
|
+
* in the same directory.
|
|
54
|
+
*/
|
|
55
|
+
export function selectConfigFiles(names: Iterable<string>, binary?: string): string[] {
|
|
56
|
+
const kept = [...names].filter((n) => isConfigFileFor(n, binary));
|
|
57
|
+
const present = new Set(kept);
|
|
58
|
+
return kept
|
|
59
|
+
.filter((n) => {
|
|
60
|
+
if (n.endsWith(".tf")) return !present.has(`${n.slice(0, -".tf".length)}.tofu`);
|
|
61
|
+
if (n.endsWith(".tf.json")) return !present.has(`${n.slice(0, -".tf.json".length)}.tofu.json`);
|
|
62
|
+
return true;
|
|
63
|
+
})
|
|
64
|
+
.sort();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The configuration files directly under `dir` a root run by `binary` reads,
|
|
69
|
+
* by name, in filename order. Non-recursive, matching Terraform's module
|
|
70
|
+
* scoping; directories named like a file are left out. Throws when `dir`
|
|
71
|
+
* cannot be read.
|
|
72
|
+
*/
|
|
73
|
+
export function listConfigFiles(dir: string, binary?: string): string[] {
|
|
74
|
+
const files = readdirSync(dir).filter((n) => {
|
|
75
|
+
if (!isConfigFileFor(n, binary)) return false;
|
|
76
|
+
try {
|
|
77
|
+
return statSync(join(dir, n)).isFile();
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
return selectConfigFiles(files, binary);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** {@link listConfigFiles}, empty when `dir` cannot be read. */
|
|
86
|
+
export function listConfigFilesSafe(dir: string, binary?: string): string[] {
|
|
87
|
+
try {
|
|
88
|
+
return listConfigFiles(dir, binary);
|
|
89
|
+
} catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The nested blocks a JSON-syntax body can carry that hcl2json would give as
|
|
96
|
+
* arrays, by block type: each entry is the nested block's label count and its
|
|
97
|
+
* own nested blocks. Anything not named here is an attribute and is kept as
|
|
98
|
+
* written, since JSON syntax cannot tell a nested block from an object
|
|
99
|
+
* attribute without the provider's schema.
|
|
100
|
+
*/
|
|
101
|
+
interface BlockShape {
|
|
102
|
+
labels: number;
|
|
103
|
+
nested?: Record<string, BlockShape>;
|
|
104
|
+
/**
|
|
105
|
+
* Attributes JSON syntax writes as a bare expression in a string (a type,
|
|
106
|
+
* a reference), where HCL writes the expression itself and hcl2json gives
|
|
107
|
+
* it as `"${...}"`. Their strings are wrapped the same way.
|
|
108
|
+
*/
|
|
109
|
+
exprs?: readonly string[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const LIFECYCLE: BlockShape = { labels: 0, exprs: ["ignore_changes", "replace_triggered_by"] };
|
|
113
|
+
const RESOURCE_NESTED: Record<string, BlockShape> = {
|
|
114
|
+
lifecycle: LIFECYCLE,
|
|
115
|
+
provisioner: { labels: 1, nested: { connection: { labels: 0 } } },
|
|
116
|
+
connection: { labels: 0 },
|
|
117
|
+
};
|
|
118
|
+
const REFERS = ["provider", "depends_on"];
|
|
119
|
+
|
|
120
|
+
const TOP_LEVEL: Record<string, BlockShape> = {
|
|
121
|
+
terraform: {
|
|
122
|
+
labels: 0,
|
|
123
|
+
nested: {
|
|
124
|
+
backend: { labels: 1 },
|
|
125
|
+
cloud: { labels: 0, nested: { workspaces: { labels: 0 } } },
|
|
126
|
+
required_providers: { labels: 0 },
|
|
127
|
+
provider_meta: { labels: 1 },
|
|
128
|
+
live: { labels: 0 },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
locals: { labels: 0 },
|
|
132
|
+
provider: { labels: 1 },
|
|
133
|
+
module: { labels: 1, exprs: ["depends_on", "providers"] },
|
|
134
|
+
variable: { labels: 1, nested: { validation: { labels: 0 } }, exprs: ["type"] },
|
|
135
|
+
output: { labels: 1, nested: { precondition: { labels: 0 } }, exprs: ["depends_on"] },
|
|
136
|
+
resource: { labels: 2, nested: RESOURCE_NESTED, exprs: REFERS },
|
|
137
|
+
data: { labels: 2, nested: { lifecycle: LIFECYCLE }, exprs: REFERS },
|
|
138
|
+
moved: { labels: 0, exprs: ["from", "to"] },
|
|
139
|
+
import: { labels: 0, exprs: ["to", "provider"] },
|
|
140
|
+
removed: { labels: 0, nested: { lifecycle: LIFECYCLE, provisioner: RESOURCE_NESTED.provisioner! }, exprs: ["from"] },
|
|
141
|
+
check: { labels: 1, nested: { data: { labels: 2 }, assert: { labels: 0 } } },
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const isObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
145
|
+
|
|
146
|
+
/** A bare expression string as hcl2json writes the expression: `aws.west` becomes `${aws.west}`; a template is kept. */
|
|
147
|
+
function asExpression(v: unknown): unknown {
|
|
148
|
+
if (typeof v === "string") return v.includes("${") ? v : `\${${v}}`;
|
|
149
|
+
if (Array.isArray(v)) return v.map(asExpression);
|
|
150
|
+
if (isObject(v)) return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, asExpression(x)]));
|
|
151
|
+
return v;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A JSON body with its `"//"` comment keys dropped (Terraform ignores them), its known nested blocks wrapped and its expression attributes written as hcl2json writes them. */
|
|
155
|
+
function normaliseBody(body: unknown, shape: BlockShape): Record<string, unknown> {
|
|
156
|
+
const out: Record<string, unknown> = {};
|
|
157
|
+
if (!isObject(body)) return out;
|
|
158
|
+
for (const [k, v] of Object.entries(body)) {
|
|
159
|
+
if (k === "//") continue;
|
|
160
|
+
const inner = shape.nested?.[k];
|
|
161
|
+
out[k] = inner ? wrapBlock(v, inner) : shape.exprs?.includes(k) ? asExpression(v) : v;
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* A block in JSON syntax as hcl2json gives the same block in HCL: one object
|
|
168
|
+
* level per label, then an array of bodies. JSON syntax writes a single body
|
|
169
|
+
* as an object or several as an array, and allows an array of objects at a
|
|
170
|
+
* label level to be merged, so both are read.
|
|
171
|
+
*/
|
|
172
|
+
function wrapBlock(value: unknown, shape: BlockShape, labels = shape.labels): unknown {
|
|
173
|
+
if (labels === 0) return (Array.isArray(value) ? value : [value]).filter(isObject).map((b) => normaliseBody(b, shape));
|
|
174
|
+
const out: Record<string, unknown> = {};
|
|
175
|
+
for (const part of Array.isArray(value) ? value : [value]) {
|
|
176
|
+
if (!isObject(part)) continue;
|
|
177
|
+
for (const [label, inner] of Object.entries(part)) {
|
|
178
|
+
if (label === "//") continue;
|
|
179
|
+
const wrapped = wrapBlock(inner, shape, labels - 1);
|
|
180
|
+
const had = out[label];
|
|
181
|
+
if (Array.isArray(had) && Array.isArray(wrapped)) out[label] = [...had, ...wrapped];
|
|
182
|
+
else if (isObject(had) && isObject(wrapped)) out[label] = mergeLabelled(had, wrapped);
|
|
183
|
+
else out[label] = wrapped;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function mergeLabelled(a: Record<string, unknown>, b: Record<string, unknown>): Record<string, unknown> {
|
|
190
|
+
const out: Record<string, unknown> = { ...a };
|
|
191
|
+
for (const [k, v] of Object.entries(b)) {
|
|
192
|
+
const had = out[k];
|
|
193
|
+
if (Array.isArray(had) && Array.isArray(v)) out[k] = [...had, ...v];
|
|
194
|
+
else if (isObject(had) && isObject(v)) out[k] = mergeLabelled(had, v);
|
|
195
|
+
else out[k] = v;
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* A `.tf.json` or `.tofu.json` document in the tree shape hcl2json gives an
|
|
202
|
+
* HCL file, so `blocksToEntities` reads both the same way. Top-level keys it
|
|
203
|
+
* does not know are kept as written. Throws on malformed JSON, as hcl2json
|
|
204
|
+
* throws on malformed HCL.
|
|
205
|
+
*/
|
|
206
|
+
export function jsonConfigTree(source: string): Record<string, unknown> {
|
|
207
|
+
const doc: unknown = JSON.parse(source);
|
|
208
|
+
if (!isObject(doc)) throw new Error("a JSON configuration file must hold an object");
|
|
209
|
+
const out: Record<string, unknown> = {};
|
|
210
|
+
for (const [k, v] of Object.entries(doc)) {
|
|
211
|
+
if (k === "//") continue;
|
|
212
|
+
const shape = TOP_LEVEL[k];
|
|
213
|
+
out[k] = shape ? wrapBlock(v, shape) : v;
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The bodies of the `live` blocks nested in the `terraform` blocks of a
|
|
220
|
+
* `.tf.json` or `.tofu.json` document, in the order written (chant#4504).
|
|
221
|
+
* The fast live readers (`report.ts`'s `estateOfDir`, `op/activities/live-detect.ts`)
|
|
222
|
+
* read HCL by text and a JSON file through this. Malformed JSON reads as no
|
|
223
|
+
* `live` block, as those readers treat any file they cannot read.
|
|
224
|
+
*/
|
|
225
|
+
export function jsonLiveBlocks(source: string): Record<string, unknown>[] {
|
|
226
|
+
let tree: Record<string, unknown>;
|
|
227
|
+
try {
|
|
228
|
+
tree = jsonConfigTree(source);
|
|
229
|
+
} catch {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
const terraform = Array.isArray(tree.terraform) ? tree.terraform : [];
|
|
233
|
+
return terraform.flatMap((t) => (isObject(t) && Array.isArray(t.live) ? t.live.filter(isObject) : []));
|
|
234
|
+
}
|