@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
package/src/report.ts
CHANGED
|
@@ -30,9 +30,14 @@
|
|
|
30
30
|
|
|
31
31
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
32
32
|
import { dirname, join } from "node:path";
|
|
33
|
-
import { changeSetDigest, composeChangeSet, type ChangeSetAction, type ChangeSetAttribute, type ChangeSetEntry, type ChangeSetPlanner } from "@intentius/chant/change-set";
|
|
33
|
+
import { changeSetDigest, composeChangeSet, type ChangeSetAction, type ChangeSetDocument, type ChangeSetAttribute, type ChangeSetEntry, type ChangeSetPlanner } from "@intentius/chant/change-set";
|
|
34
34
|
import { groupChangeSet } from "@intentius/chant/plan-summary";
|
|
35
35
|
import { plannerForBinary, terraformChangeSetPart } from "./change-set";
|
|
36
|
+
import { moduleChanges, stripModule, topModule } from "./compose/module-address";
|
|
37
|
+
import { stateResources } from "./applied-state";
|
|
38
|
+
import { isJsonConfigFile, jsonLiveBlocks, listConfigFiles } from "./hcl/files";
|
|
39
|
+
|
|
40
|
+
export { topModule } from "./compose/module-address";
|
|
36
41
|
|
|
37
42
|
type Json = Record<string, unknown>;
|
|
38
43
|
const isObject = (v: unknown): v is Json => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
@@ -139,40 +144,15 @@ export interface IndexRow {
|
|
|
139
144
|
// Slicing the composed plan into one plan per module.
|
|
140
145
|
// ---------------------------------------------------------------------------
|
|
141
146
|
|
|
142
|
-
const TOP_MODULE = /^module\.([^.[\]]+)(?:\[(?:"(?:[^"\\]|\\.)*"|\d+)\])?(?:\.|$)/;
|
|
143
|
-
|
|
144
|
-
/** The top-level module an address or module address is in, or undefined for the composed root's own. */
|
|
145
|
-
export function topModule(address: string): string | undefined {
|
|
146
|
-
return TOP_MODULE.exec(address)?.[1];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** `address` with its leading `module.<m>` (and index) taken off. */
|
|
150
|
-
function unprefix(address: string): string {
|
|
151
|
-
const m = TOP_MODULE.exec(address);
|
|
152
|
-
return m ? address.slice(m[0].length) : address;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
147
|
/** The plan's `resource_changes` that belong to module `m` (undefined: the root's own), each with the module taken off its address. */
|
|
156
148
|
export function slicePlan(plan: unknown, m: string | undefined): Json {
|
|
157
149
|
const p = isObject(plan) ? plan : {};
|
|
158
|
-
|
|
159
|
-
const mine = list.filter((r) => isObject(r) && topModule(String(r.module_address ?? r.address ?? "")) === m);
|
|
160
|
-
const resource_changes = mine.map((r) => {
|
|
161
|
-
const rc = r as Json;
|
|
162
|
-
const out: Json = { ...rc, address: unprefix(String(rc.address)) };
|
|
163
|
-
if (typeof rc.previous_address === "string") out.previous_address = unprefix(rc.previous_address);
|
|
164
|
-
if (typeof rc.module_address === "string") {
|
|
165
|
-
const rest = unprefix(rc.module_address);
|
|
166
|
-
if (rest === "" || rest === rc.module_address) delete out.module_address;
|
|
167
|
-
else out.module_address = rest;
|
|
168
|
-
}
|
|
169
|
-
return out;
|
|
170
|
-
});
|
|
171
|
-
return { ...(p.errored === true ? { errored: true } : {}), resource_changes };
|
|
150
|
+
return { ...(p.errored === true ? { errored: true } : {}), resource_changes: moduleChanges(p.resource_changes, m) };
|
|
172
151
|
}
|
|
173
152
|
|
|
174
153
|
// ---------------------------------------------------------------------------
|
|
175
|
-
// Applied values: the state after the apply gives the values the plan left unknown
|
|
154
|
+
// Applied values: the state after the apply gives the values the plan left unknown,
|
|
155
|
+
// read through `./applied-state`, as the composed estate run reads them.
|
|
176
156
|
// ---------------------------------------------------------------------------
|
|
177
157
|
|
|
178
158
|
/** Whether a sensitivity map marks anything under it `true`. */
|
|
@@ -183,25 +163,6 @@ function marks(v: unknown): boolean {
|
|
|
183
163
|
return false;
|
|
184
164
|
}
|
|
185
165
|
|
|
186
|
-
/** Every resource of a `show -json` state, by address. A deposed object is left out. */
|
|
187
|
-
function stateResources(show: unknown): Map<string, { values: Json; sensitive: Json }> {
|
|
188
|
-
const out = new Map<string, { values: Json; sensitive: Json }>();
|
|
189
|
-
const walk = (mod: unknown): void => {
|
|
190
|
-
if (!isObject(mod)) return;
|
|
191
|
-
for (const r of Array.isArray(mod.resources) ? mod.resources : []) {
|
|
192
|
-
if (!isObject(r) || typeof r.address !== "string" || r.deposed_key !== undefined) continue;
|
|
193
|
-
out.set(r.address, { values: isObject(r.values) ? r.values : {}, sensitive: isObject(r.sensitive_values) ? r.sensitive_values : {} });
|
|
194
|
-
}
|
|
195
|
-
for (const c of Array.isArray(mod.child_modules) ? mod.child_modules : []) walk(c);
|
|
196
|
-
};
|
|
197
|
-
const root = isObject(show) ? show : {};
|
|
198
|
-
// A plan's `prior_state` is the state; a `show -json` of state has `values` itself.
|
|
199
|
-
const state = isObject(root.prior_state) ? root.prior_state : root;
|
|
200
|
-
const values = isObject(state.values) ? state.values : {};
|
|
201
|
-
walk(values.root_module);
|
|
202
|
-
return out;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
166
|
/** Each unknown attribute given the value the state holds. One the state lacks stays unknown; a sensitive one stays `sensitive`. */
|
|
206
167
|
function resolveUnknowns(attributes: ChangeSetAttribute[], res: { values: Json; sensitive: Json } | undefined): ChangeSetAttribute[] {
|
|
207
168
|
if (!res || !attributes.some((a) => a.unknown)) return attributes;
|
|
@@ -230,15 +191,27 @@ export interface ReportRead {
|
|
|
230
191
|
composed: true;
|
|
231
192
|
}
|
|
232
193
|
|
|
233
|
-
/**
|
|
234
|
-
|
|
194
|
+
/**
|
|
195
|
+
* The estate named by the first `live { estate = "..." }` block in a
|
|
196
|
+
* directory's configuration files, in filename order, or undefined. A `live`
|
|
197
|
+
* block takes effect only under choudoufu, so the files are the ones
|
|
198
|
+
* choudoufu reads (`.tf`, `.tf.json`, `.tofu` and `.tofu.json`, chant#4457)
|
|
199
|
+
* unless `binary` names another. A JSON-syntax file's `live` block is the one
|
|
200
|
+
* nested in its `terraform` block (chant#4504).
|
|
201
|
+
*/
|
|
202
|
+
export function estateOfDir(dir: string, binary = "choudoufu"): string | undefined {
|
|
235
203
|
let files: string[];
|
|
236
204
|
try {
|
|
237
|
-
files =
|
|
205
|
+
files = listConfigFiles(dir, binary);
|
|
238
206
|
} catch {
|
|
239
207
|
return undefined;
|
|
240
208
|
}
|
|
241
209
|
for (const f of files) {
|
|
210
|
+
if (isJsonConfigFile(f)) {
|
|
211
|
+
const estate = jsonLiveBlocks(readFileSync(join(dir, f), "utf8")).map((b) => b.estate).find((e): e is string => typeof e === "string" && e !== "");
|
|
212
|
+
if (estate) return estate;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
242
215
|
const text = readFileSync(join(dir, f), "utf8").replace(/^\s*(#|\/\/).*$/gm, "");
|
|
243
216
|
const live = /\blive\s*\{/.exec(text);
|
|
244
217
|
if (!live) continue;
|
|
@@ -309,7 +282,7 @@ function unknownOutputsOf(plan: Json, u: string): Set<string> {
|
|
|
309
282
|
const data = parts[0] === "data";
|
|
310
283
|
const [type, rname, attr] = data ? [parts[1], parts[2], parts[3]] : [parts[0], parts[1], parts[2]];
|
|
311
284
|
if (!type || !rname) continue;
|
|
312
|
-
const unprefixed = (c: Json): string =>
|
|
285
|
+
const unprefixed = (c: Json): string => stripModule(String(c.address)).replace(/\[[^\]]*\]$/, "");
|
|
313
286
|
for (const c of changes) {
|
|
314
287
|
if (unprefixed(c) !== `${data ? "data." : ""}${type}.${rname}`) continue;
|
|
315
288
|
const after = isObject(c.change) ? c.change.after_unknown : undefined;
|
|
@@ -494,6 +467,8 @@ function rawChanges(plan: Json): Map<string, RawChange> {
|
|
|
494
467
|
|
|
495
468
|
export interface TerraformReport {
|
|
496
469
|
report: Json;
|
|
470
|
+
/** The change set whose digest `report.change_set` names: what behold reads beside the report. */
|
|
471
|
+
changeSet: ChangeSetDocument;
|
|
497
472
|
/** The run's directory, relative to the project's own index: `yyyy/mm/<commit>/<stage>`. */
|
|
498
473
|
path: string;
|
|
499
474
|
row: IndexRow;
|
|
@@ -680,7 +655,7 @@ export function terraformReport(input: TerraformReportInput): TerraformReport {
|
|
|
680
655
|
...(run.pull_request_url ? { pull_request_url: run.pull_request_url } : {}),
|
|
681
656
|
...(run.job_url ? { job_url: run.job_url } : {}),
|
|
682
657
|
};
|
|
683
|
-
return { report, path, row };
|
|
658
|
+
return { report, changeSet: doc, path, row };
|
|
684
659
|
}
|
|
685
660
|
|
|
686
661
|
// ---------------------------------------------------------------------------
|
|
@@ -716,7 +691,8 @@ export interface ReportStore {
|
|
|
716
691
|
}
|
|
717
692
|
|
|
718
693
|
/**
|
|
719
|
-
* Write the run's `report.json` and
|
|
694
|
+
* Write the run's `report.json` and `change-set.json` (the document whose
|
|
695
|
+
* digest the report names, as `chant change-set` writes it), and add its row to the project's index
|
|
720
696
|
* (`<project>/index.json`, paths relative to it) and to the prefix's
|
|
721
697
|
* (`index.json`, paths starting at the project), the two places terragucci
|
|
722
698
|
* writes. Returns the keys written.
|
|
@@ -729,6 +705,7 @@ export async function writeReport(store: ReportStore, r: TerraformReport): Promi
|
|
|
729
705
|
keys.push(key);
|
|
730
706
|
};
|
|
731
707
|
await put(`${project}/${r.path}/report.json`, r.report);
|
|
708
|
+
await put(`${project}/${r.path}/change-set.json`, r.changeSet);
|
|
732
709
|
for (const [key, path] of [[`${project}/index.json`, r.path], ["index.json", `${project}/${r.path}`]] as const) {
|
|
733
710
|
await put(key, addToIndex(await store.read(key), { ...r.row, path }));
|
|
734
711
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `signalOnly` root (#4484, C5 of #4477). The dns member declares the azure
|
|
3
|
+
* estate's root only so an azure change brings dns into the change. Change
|
|
4
|
+
* detection counts it; the build and the live reads leave it out, so dns's
|
|
5
|
+
* graph draws only dns's own root and dns's live reads never list azure.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
12
|
+
import { terraformConfigSchema, ownRoots } from "./config";
|
|
13
|
+
import { terraformPlugin } from "./plugin";
|
|
14
|
+
import { liveRootNames } from "./describe-resources";
|
|
15
|
+
|
|
16
|
+
const scratch: string[] = [];
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
for (const d of scratch) rmSync(d, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const TERRAFORM: { binary: "choudoufu"; roots: Record<string, { dir: string; signalOnly?: boolean }> } = {
|
|
22
|
+
binary: "choudoufu",
|
|
23
|
+
roots: {
|
|
24
|
+
dns: { dir: "estate" },
|
|
25
|
+
azure: { dir: "../azure", signalOnly: true },
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** A workspace slice: `dns/` (the chant member, with its own root under `estate/`) beside `azure/`. */
|
|
30
|
+
function estate(): string {
|
|
31
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-signal-only-")));
|
|
32
|
+
scratch.push(root);
|
|
33
|
+
const files: Record<string, string> = {
|
|
34
|
+
"dns/chant.config.json": JSON.stringify({ lexicons: ["terraform"], terraform: TERRAFORM }),
|
|
35
|
+
"dns/estate/main.tf": 'resource "null_resource" "zone" {}\n',
|
|
36
|
+
"dns/estate/estate.chdf.hcl": 'estate = "dns"\n',
|
|
37
|
+
"azure/main.tf": 'resource "null_resource" "vnet" {}\n',
|
|
38
|
+
"azure/estate.chdf.hcl": 'estate = "azure"\n',
|
|
39
|
+
};
|
|
40
|
+
for (const [path, text] of Object.entries(files)) {
|
|
41
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
42
|
+
writeFileSync(join(root, path), text);
|
|
43
|
+
}
|
|
44
|
+
return join(root, "dns");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("a signalOnly root (#4484)", () => {
|
|
48
|
+
test("the config schema takes the marker", () => {
|
|
49
|
+
expect(terraformConfigSchema.safeParse(TERRAFORM).success).toBe(true);
|
|
50
|
+
expect(Object.keys(ownRoots(TERRAFORM.roots))).toEqual(["dns"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("the build draws only dns's own root, so the azure estate is not drawn under dns", async () => {
|
|
54
|
+
const dns = estate();
|
|
55
|
+
const { entities } = await terraformPlugin.buildRoots!({ projectRoot: dns, config: { terraform: TERRAFORM }, entities: new Map() });
|
|
56
|
+
const keys = [...entities.keys()];
|
|
57
|
+
expect(keys.length).toBeGreaterThan(0);
|
|
58
|
+
expect(keys.every((k) => k.startsWith("dns/"))).toBe(true);
|
|
59
|
+
expect(keys.some((k) => k.startsWith("azure/"))).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("the live reads list dns's root and not the azure estate", async () => {
|
|
63
|
+
const dns = estate();
|
|
64
|
+
expect(await liveRootNames(dns)).toEqual([{ root: "dns", estate: "dns" }]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("an azure change still brings dns into the change", async () => {
|
|
68
|
+
const dns = estate();
|
|
69
|
+
const touched = await terraformPlugin.changedUnits!({ projectRoot: dns, config: { terraform: TERRAFORM }, changedFiles: ["../azure/main.tf"] });
|
|
70
|
+
expect(touched).toEqual(["azure"]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -33,7 +33,7 @@ export default {
|
|
|
33
33
|
|
|
34
34
|
## What a build produces
|
|
35
35
|
|
|
36
|
-
Every block of every configured root becomes one entity, keyed `<root>/<address>`: `terraform { }` -> `Terraform::Terraform`, `provider "x" {}` -> `Terraform::Provider`, `resource "x" "y" {}` -> `Terraform::Resource`, `data "x" "y" {}` -> `Terraform::Data`, `module "x" {}` -> `Terraform::Module`, `variable "x" {}` -> `Terraform::Variable`, `output "x" {}` -> `Terraform::Output`, `locals {}` -> `Terraform::Locals`. Each carries `props.address`, `props.body` (the block, verbatim), `props.file` and `props.root`. A root whose `dir` doesn't exist, or whose HCL the parser refuses, warns and contributes no entities — the rest of the build is unaffected.
|
|
36
|
+
Every block of every configured root becomes one entity, keyed `<root>/<address>`: `terraform { }` -> `Terraform::Terraform`, `provider "x" {}` -> `Terraform::Provider`, `resource "x" "y" {}` -> `Terraform::Resource`, `data "x" "y" {}` -> `Terraform::Data`, `module "x" {}` -> `Terraform::Module`, `variable "x" {}` -> `Terraform::Variable`, `output "x" {}` -> `Terraform::Output`, `locals {}` -> `Terraform::Locals`, `moved {}` -> `Terraform::Moved`, `import {}` -> `Terraform::Import`, `removed {}` -> `Terraform::Removed`, `check "x" {}` -> `Terraform::Check`. Each carries `props.address`, `props.body` (the block, verbatim), `props.file` and `props.root`. A root whose `dir` doesn't exist, or whose HCL the parser refuses, warns and contributes no entities — the rest of the build is unaffected.
|
|
37
37
|
|
|
38
38
|
## The one thing to check before anything else
|
|
39
39
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Azure Blob Storage client an azurerm backend's state uses: chant
|
|
3
|
+
* core's (#4554, `@intentius/chant/object-store`): a blob's properties and
|
|
4
|
+
* metadata, a snapshot, a block blob put, and the blob lease the backend
|
|
5
|
+
* locks a state with, signed with Shared Key, a SAS, or an Entra token for
|
|
6
|
+
* the job's OIDC identity.
|
|
7
|
+
*/
|
|
8
|
+
export { AZURE_AUTHORITY, AZURE_VERSION, AzureBlobClient, sharedKey, type AzureLocation, type AzureOidc, type AzureTarget, type BlobHead } from "@intentius/chant/object-store";
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One backend verdict for compose and state (chant#4478). The composition's
|
|
3
|
+
* `backend` fallback (#4438) and the state refusals (#4437) both come from
|
|
4
|
+
* `lockRefusal(readBackend(...))`, so the same root gets the same answer from
|
|
5
|
+
* each, before init (the code and flags) and after (what init recorded,
|
|
6
|
+
* compared with the code).
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
12
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
13
|
+
import { compositionFor, type CompositionInput } from "../compose";
|
|
14
|
+
import { rootState } from "../compose/hcl";
|
|
15
|
+
import type { TerraformConfig } from "../config";
|
|
16
|
+
import { LOCKABLE_BACKENDS, LOCKS, cliBackendConfig, declaredBackend, isRefused, lockRefusal, readBackend, stateObject } from "./backend";
|
|
17
|
+
|
|
18
|
+
const made: string[] = [];
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
for (const d of made) rmSync(d, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function project(files: Record<string, string>): string {
|
|
24
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-4478-"));
|
|
25
|
+
made.push(dir);
|
|
26
|
+
for (const [path, text] of Object.entries(files)) {
|
|
27
|
+
mkdirSync(dirname(join(dir, path)), { recursive: true });
|
|
28
|
+
writeFileSync(join(dir, path), text);
|
|
29
|
+
}
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const EMPTY = 'resource "terraform_data" "x" {}\n';
|
|
34
|
+
const tf = (name: string): DriverComponent => ({ name, dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: name }] }] });
|
|
35
|
+
const record = (type: string, config: Record<string, unknown>): string => JSON.stringify({ version: 3, backend: { type, config, hash: 1 } });
|
|
36
|
+
|
|
37
|
+
/** Credentials for every lockable backend, so a lockable root resolves to a state rather than a credentials refusal. */
|
|
38
|
+
const CREDS: NodeJS.ProcessEnv = { AWS_ACCESS_KEY_ID: "AK", AWS_SECRET_ACCESS_KEY: "SK", GOOGLE_OAUTH_ACCESS_TOKEN: "t", ARM_ACCESS_KEY: Buffer.from("k").toString("base64") };
|
|
39
|
+
|
|
40
|
+
interface Case {
|
|
41
|
+
name: string;
|
|
42
|
+
/** Root `c`'s files, beside the plain roots `a` and `b`. */
|
|
43
|
+
files: Record<string, string>;
|
|
44
|
+
env?: NodeJS.ProcessEnv;
|
|
45
|
+
terraform?: Partial<TerraformConfig>;
|
|
46
|
+
/** The refusal kind both sides give, or undefined when chant locks it. */
|
|
47
|
+
refused?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const s3 = (body: string): string => `terraform {\n backend "s3" {\n bucket = "b"\n key = "c.tfstate"\n${body} }\n}\n`;
|
|
51
|
+
|
|
52
|
+
const CASES: Case[] = [
|
|
53
|
+
{ name: "s3 with use_lockfile = true in the code", files: { "main.tf": s3(" use_lockfile = true\n") } },
|
|
54
|
+
{ name: "s3 with no lock file", files: { "main.tf": s3("") }, refused: "no-lockfile" },
|
|
55
|
+
{ name: "s3 whose use_lockfile = true reaches init only through TF_CLI_ARGS_init", files: { "main.tf": s3("") }, env: { TF_CLI_ARGS_init: "-input=false -backend-config=use_lockfile=true" } },
|
|
56
|
+
{ name: "s3 whose use_lockfile = true is in backendConfig", files: { "main.tf": s3("") }, terraform: { roots: { a: { dir: "roots/a" }, b: { dir: "roots/b" }, c: { dir: "roots/c", backendConfig: { use_lockfile: "true" } } } } },
|
|
57
|
+
{ name: "gcs", files: { "main.tf": 'terraform {\n backend "gcs" {\n bucket = "b"\n prefix = "c"\n }\n}\n' } },
|
|
58
|
+
{ name: "azurerm", files: { "main.tf": 'terraform {\n backend "azurerm" {\n storage_account_name = "acct"\n container_name = "tfstate"\n key = "c.tfstate"\n }\n}\n' } },
|
|
59
|
+
{ name: "local, declared", files: { "main.tf": 'terraform {\n backend "local" {}\n}\n' } },
|
|
60
|
+
{ name: "local, by declaring nothing", files: { "main.tf": EMPTY } },
|
|
61
|
+
{ name: "a cloud block", files: { "main.tf": 'terraform {\n cloud {\n organization = "o"\n }\n}\n' }, refused: "cloud" },
|
|
62
|
+
{ name: "a cloud block in a .tofu file of a tofu root", files: { "main.tofu": 'terraform {\n cloud {\n organization = "o"\n }\n}\n' }, terraform: { binary: "tofu" }, refused: "cloud" },
|
|
63
|
+
{ name: "another backend", files: { "main.tf": 'terraform {\n backend "consul" {\n path = "c"\n }\n}\n' }, refused: "backend" },
|
|
64
|
+
{
|
|
65
|
+
name: "s3 whose use_lockfile = true is only in a backend-config file (chant#4459)",
|
|
66
|
+
files: { "main.tf": s3(""), "backend.hcl": "# the lock\nuse_lockfile = true\n" },
|
|
67
|
+
terraform: { roots: { a: { dir: "roots/a" }, b: { dir: "roots/b" }, c: { dir: "roots/c", backendConfigFiles: ["backend.hcl"] } } },
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "init recorded another bucket than the code names",
|
|
71
|
+
files: { "main.tf": s3(" use_lockfile = true\n"), ".terraform/terraform.tfstate": record("s3", { bucket: "elsewhere", key: "c.tfstate", use_lockfile: true }) },
|
|
72
|
+
refused: "disagree",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "init recorded s3 and the code now names gcs",
|
|
76
|
+
files: { "main.tf": 'terraform {\n backend "gcs" {\n bucket = "b"\n }\n}\n', ".terraform/terraform.tfstate": record("s3", { bucket: "b", key: "k", use_lockfile: true }) },
|
|
77
|
+
refused: "disagree",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "init recorded what the code names, plus defaults the code leaves out",
|
|
81
|
+
files: { "main.tf": s3(" use_lockfile = true\n"), ".terraform/terraform.tfstate": record("s3", { bucket: "b", key: "c.tfstate", use_lockfile: true, region: "us-east-1", workspace_key_prefix: null }) },
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
function both(c: Case): { fallback?: string; state: ReturnType<typeof stateObject> } {
|
|
86
|
+
const dir = project({ "roots/a/main.tf": EMPTY, "roots/b/main.tf": EMPTY, ...Object.fromEntries(Object.entries(c.files).map(([f, t]) => [`roots/c/${f}`, t])) });
|
|
87
|
+
const env = { ...CREDS, ...c.env };
|
|
88
|
+
const terraform: TerraformConfig = { roots: { a: { dir: "roots/a" }, b: { dir: "roots/b" }, c: { dir: "roots/c" } }, compose: { enabled: true }, ...c.terraform } as TerraformConfig;
|
|
89
|
+
const input: CompositionInput = { projectRoot: dir, components: [tf("a"), tf("b"), tf("c")], env, terraform };
|
|
90
|
+
const fallback = compositionFor(input).fallbacks.find((f) => f.member === "c" && f.kind === "backend")?.reason;
|
|
91
|
+
const state = stateObject(join(dir, "roots/c"), env, undefined, terraform.binary, {
|
|
92
|
+
...(terraform.roots.c?.backendConfig ? { backendConfig: terraform.roots.c.backendConfig } : {}),
|
|
93
|
+
...(terraform.roots.c?.backendConfigFiles ? { backendConfigFiles: terraform.roots.c.backendConfigFiles } : {}),
|
|
94
|
+
});
|
|
95
|
+
return { ...(fallback ? { fallback } : {}), state };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe("compose and state give one verdict per root (chant#4478)", () => {
|
|
99
|
+
for (const c of CASES) {
|
|
100
|
+
test(c.name, () => {
|
|
101
|
+
const { fallback, state } = both(c);
|
|
102
|
+
if (c.refused === undefined) {
|
|
103
|
+
expect(fallback).toBeUndefined();
|
|
104
|
+
expect(isRefused(state)).toBe(false);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
expect(state).toMatchObject({ refused: c.refused });
|
|
108
|
+
expect(fallback).toBe(`\`c\`: ${isRefused(state) ? state.reason : ""}`);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
test("a disagreement names both sides", () => {
|
|
113
|
+
const { state } = both(CASES.find((c) => c.name.startsWith("init recorded another bucket"))!);
|
|
114
|
+
expect(isRefused(state) && state.reason).toContain('bucket = "b" in the code and flags, bucket = "elsewhere" in what init recorded');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("a disagreement over a secret names the key, not its values", () => {
|
|
118
|
+
const dir = project({ "main.tf": 'terraform {\n backend "s3" {\n bucket = "b"\n key = "k"\n use_lockfile = true\n secret_key = "one"\n }\n}\n' });
|
|
119
|
+
const reading = readBackend(dir, { recorded: { type: "s3", config: { bucket: "b", key: "k", use_lockfile: true, secret_key: "two" } } });
|
|
120
|
+
expect(reading.disagreement).toContain("secret_key (value hidden)");
|
|
121
|
+
expect(reading.disagreement).not.toContain("one");
|
|
122
|
+
expect(reading.disagreement).not.toContain("two");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("the parts of the verdict", () => {
|
|
127
|
+
test("the lockable list is the one LOCKS spells", () => {
|
|
128
|
+
expect(LOCKABLE_BACKENDS).toEqual(["s3", "gcs", "azurerm", "local"]);
|
|
129
|
+
expect(LOCKS).toBe("chant locks and writes s3 (with use_lockfile = true), gcs, azurerm and local states");
|
|
130
|
+
for (const type of LOCKABLE_BACKENDS) expect(lockRefusal({ backend: { type, config: { use_lockfile: true } } })).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("TF_CLI_ARGS and TF_CLI_ARGS_init -backend-config flags, pairs and files, later over earlier", () => {
|
|
134
|
+
expect(cliBackendConfig({ TF_CLI_ARGS: "-backend-config=region=eu-west-1 -backend-config backend.hcl", TF_CLI_ARGS_init: '-backend-config="region=us-east-2" --backend-config=use_lockfile=true' })).toMatchObject({
|
|
135
|
+
pairs: { region: "us-east-2", use_lockfile: "true" },
|
|
136
|
+
files: ["backend.hcl"],
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("the declared backend: code, then TF_CLI_ARGS*, then backendConfig", () => {
|
|
141
|
+
const dir = project({ "main.tf": 'terraform {\n backend "s3" {\n bucket = "b"\n key = "k"\n assume_role {\n role_arn = "nested"\n }\n }\n}\n' });
|
|
142
|
+
expect(declaredBackend(dir, { env: { TF_CLI_ARGS_init: "-backend-config=key=from-env" }, backendConfig: { use_lockfile: "true" } })).toEqual({
|
|
143
|
+
type: "s3",
|
|
144
|
+
config: { bucket: "b", key: "from-env", use_lockfile: true },
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("the declared backend in Terraform's JSON syntax", () => {
|
|
149
|
+
const dir = project({ "main.tf.json": JSON.stringify({ terraform: { backend: { s3: { bucket: "b", key: "k", use_lockfile: true } } } }) });
|
|
150
|
+
expect(declaredBackend(dir)).toEqual({ type: "s3", config: { bucket: "b", key: "k", use_lockfile: true } });
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("compose's state address comes from the same verdict (chant#4478 round 2)", () => {
|
|
155
|
+
const S3_NO_KEY = 'terraform {\n backend "s3" {\n bucket = "b"\n use_lockfile = true\n }\n}\n';
|
|
156
|
+
const reader = 'data "terraform_remote_state" "a" {\n backend = "s3"\n config = {\n bucket = "b"\n key = "a.tfstate"\n }\n}\n\nresource "terraform_data" "x" {\n input = data.terraform_remote_state.a.outputs.id\n}\n';
|
|
157
|
+
|
|
158
|
+
test("a key only TF_CLI_ARGS_init gives is the root's state address, as stateObject reads it", () => {
|
|
159
|
+
const dir = project({ "main.tf": S3_NO_KEY });
|
|
160
|
+
const env = { ...CREDS, TF_CLI_ARGS_init: "-backend-config=key=from-init-args.tfstate" };
|
|
161
|
+
expect(rootState(dir, "app", { env, recorded: null }).own).toEqual({ bucket: "b", key: "from-init-args.tfstate" });
|
|
162
|
+
expect(stateObject(dir, env, undefined)).toMatchObject({ backend: "s3", bucket: "b", key: "from-init-args.tfstate" });
|
|
163
|
+
expect(rootState(dir, "app", { env: {}, recorded: null }).own).toBeUndefined();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("a key TF_CLI_ARGS gives is overridden by TF_CLI_ARGS_init, as init applies them", () => {
|
|
167
|
+
const dir = project({ "main.tf": S3_NO_KEY });
|
|
168
|
+
const env = { TF_CLI_ARGS: "-backend-config=key=first.tfstate", TF_CLI_ARGS_init: "-backend-config=key=second.tfstate" };
|
|
169
|
+
expect(rootState(dir, "app", { env, recorded: null }).own).toEqual({ bucket: "b", key: "second.tfstate" });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("after init, the recorded configuration names the state", () => {
|
|
173
|
+
const dir = project({ "main.tf": S3_NO_KEY, ".terraform/terraform.tfstate": record("s3", { bucket: "b", key: "recorded.tfstate", use_lockfile: true, region: null }) });
|
|
174
|
+
expect(rootState(dir, "app", { env: {} }).own).toEqual({ bucket: "b", key: "recorded.tfstate" });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("a composition lines a read up with a member whose key comes only from TF_CLI_ARGS_init", () => {
|
|
178
|
+
const dir = project({ "roots/a/main.tf": S3_NO_KEY, "roots/b/main.tf": S3_NO_KEY + reader });
|
|
179
|
+
const c = compositionFor({
|
|
180
|
+
projectRoot: dir,
|
|
181
|
+
env: { TF_CLI_ARGS_init: "-backend-config=key=a.tfstate" },
|
|
182
|
+
components: [tf("a"), tf("b")],
|
|
183
|
+
terraform: { roots: { a: { dir: "roots/a" }, b: { dir: "roots/b" } }, compose: { enabled: true } },
|
|
184
|
+
});
|
|
185
|
+
expect(c.reads.get("b")).toEqual([{ via: "remote-state", upstream: "a", name: "a" }]);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a root's state is (#4437). Carried over from terragucci at c493e0e:
|
|
3
|
+
* test/state-versions.test.ts "where a root's state is" and
|
|
4
|
+
* test/state-stores.test.ts "where a gcs or azurerm root's state is", with
|
|
5
|
+
* the refusals chant adds (no lock file, a cloud block, any other backend).
|
|
6
|
+
*/
|
|
7
|
+
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
import { initialisedBackend, isRefused, s3StateKey, stateLocation, stateObject, workspaceOf } from "./backend";
|
|
13
|
+
|
|
14
|
+
const tmp = (): string => mkdtempSync(join(tmpdir(), "chant-state-"));
|
|
15
|
+
const write = (dir: string, files: Record<string, string>): string => {
|
|
16
|
+
for (const [f, text] of Object.entries(files)) {
|
|
17
|
+
mkdirSync(dirname(join(dir, f)), { recursive: true });
|
|
18
|
+
writeFileSync(join(dir, f), text);
|
|
19
|
+
}
|
|
20
|
+
return dir;
|
|
21
|
+
};
|
|
22
|
+
/** A root whose `.terraform` records `type` and `config`, as init leaves it. */
|
|
23
|
+
const initialised = (type: string, config: Record<string, unknown>, files: Record<string, string> = {}): string =>
|
|
24
|
+
write(tmp(), { ".terraform/terraform.tfstate": JSON.stringify({ version: 3, backend: { type, config, hash: 1 } }), ...files });
|
|
25
|
+
|
|
26
|
+
const ENV = { AWS_ACCESS_KEY_ID: "AK", AWS_SECRET_ACCESS_KEY: "SK", AWS_REGION: "us-east-1" };
|
|
27
|
+
const S3_CONFIG = { bucket: "acme-state", key: "envs/dev/platform.tfstate", region: "eu-west-1", use_lockfile: true, endpoints: { s3: "http://floci:4566", dynamodb: null }, access_key: null, workspace_key_prefix: null };
|
|
28
|
+
const GCS = { bucket: "acme-state", prefix: "envs/dev/app", storage_custom_endpoint: "http://gcs.test/storage/v1/", access_token: "backend-token" };
|
|
29
|
+
const AZ = { storage_account_name: "acme", container_name: "tfstate", key: "app.tfstate", access_key: Buffer.from("key").toString("base64") };
|
|
30
|
+
|
|
31
|
+
describe("where an s3 or local root's state is", () => {
|
|
32
|
+
it("reads the backend init recorded, s3 with its bucket, key, region and endpoint", () => {
|
|
33
|
+
const dir = initialised("s3", S3_CONFIG);
|
|
34
|
+
expect(initialisedBackend(dir)?.type).toBe("s3");
|
|
35
|
+
const o = stateObject(dir, ENV);
|
|
36
|
+
expect(o).toMatchObject({ backend: "s3", bucket: "acme-state", key: "envs/dev/platform.tfstate", region: "eu-west-1", endpoint: "http://floci:4566", lockfile: true });
|
|
37
|
+
expect(stateLocation(o)).toBe("s3://acme-state/envs/dev/platform.tfstate");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("takes the endpoint from AWS_ENDPOINT_URL when the backend names none, and the key of another workspace", () => {
|
|
41
|
+
const dir = initialised("s3", { ...S3_CONFIG, endpoints: null, workspace_key_prefix: "ws" });
|
|
42
|
+
expect(stateObject(dir, { ...ENV, AWS_ENDPOINT_URL: "http://floci:4566/", TF_WORKSPACE: "blue" })).toMatchObject({ endpoint: "http://floci:4566", key: "ws/blue/envs/dev/platform.tfstate" });
|
|
43
|
+
expect(s3StateKey("k", "default")).toBe("k");
|
|
44
|
+
expect(s3StateKey("k", "green")).toBe("env:/green/k");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("reads the workspace select recorded, and TF_DATA_DIR in place of .terraform", () => {
|
|
48
|
+
const dir = write(tmp(), { "data/terraform.tfstate": JSON.stringify({ backend: { type: "s3", config: S3_CONFIG } }), "data/environment": "green\n" });
|
|
49
|
+
expect(workspaceOf(dir, { TF_DATA_DIR: "data" })).toBe("green");
|
|
50
|
+
expect(stateObject(dir, { ...ENV, TF_DATA_DIR: "data" })).toMatchObject({ key: "env:/green/envs/dev/platform.tfstate" });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("signs with the backend's own static keys when its configuration names them", () => {
|
|
54
|
+
const o = stateObject(initialised("s3", { ...S3_CONFIG, access_key: "BACKENDKEY", secret_key: "s" }), ENV);
|
|
55
|
+
expect(o).toMatchObject({ target: { accessKeyId: "BACKENDKEY", secretAccessKey: "s" } });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("is a local file when init recorded no backend or a local one", () => {
|
|
59
|
+
expect(stateObject(tmp(), ENV)).toEqual({ backend: "local", path: "terraform.tfstate" });
|
|
60
|
+
expect(stateObject(initialised("local", { path: "state/x.tfstate" }), ENV)).toEqual({ backend: "local", path: "state/x.tfstate" });
|
|
61
|
+
expect(stateObject(initialised("local", {}), { TF_WORKSPACE: "blue" })).toEqual({ backend: "local", path: join("terraform.tfstate.d", "blue", "terraform.tfstate") });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("where a gcs or azurerm root's state is", () => {
|
|
66
|
+
it("names a gcs state <prefix>/<workspace>.tfstate, its lock beside it, with the backend's own credentials first", () => {
|
|
67
|
+
expect(stateObject(initialised("gcs", GCS), { GOOGLE_APPLICATION_CREDENTIALS: "/job/creds.json" })).toMatchObject({ backend: "gcs", bucket: "acme-state", key: "envs/dev/app/default.tfstate", lock: "envs/dev/app/default.tflock", target: { endpoint: "http://gcs.test", accessToken: "backend-token" } });
|
|
68
|
+
expect(stateObject(initialised("gcs", { bucket: "b", prefix: "p/" }), { TF_WORKSPACE: "blue", GOOGLE_APPLICATION_CREDENTIALS: "/job/creds.json" })).toMatchObject({ key: "p/blue.tfstate", lock: "p/blue.tflock", target: { endpoint: "https://storage.googleapis.com", credentialsFile: "/job/creds.json" } });
|
|
69
|
+
expect(stateObject(initialised("gcs", { bucket: "b" }), { GOOGLE_CREDENTIALS: '{"type":"service_account"}' })).toMatchObject({ key: "default.tfstate", target: { credentialsJson: '{"type":"service_account"}' } });
|
|
70
|
+
expect(stateObject(initialised("gcs", { bucket: "b", impersonate_service_account: "sa@p.iam.gserviceaccount.com" }), { GOOGLE_OAUTH_ACCESS_TOKEN: "t" })).toMatchObject({ target: { accessToken: "t", impersonate: "sa@p.iam.gserviceaccount.com" } });
|
|
71
|
+
expect(stateObject(initialised("gcs", { bucket: "b" }), {})).toMatchObject({ backend: "gcs", refused: "credentials" });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("names an azurerm blob by its key, <key>env:<workspace> in another workspace, at the cloud's endpoint, with the key, a SAS or the job's OIDC identity", () => {
|
|
75
|
+
expect(stateObject(initialised("azurerm", AZ), {})).toMatchObject({ backend: "azurerm", account: "acme", container: "tfstate", key: "app.tfstate", endpoint: "https://acme.blob.core.windows.net", auth: { accountKey: AZ.access_key } });
|
|
76
|
+
expect(stateObject(initialised("azurerm", { ...AZ, access_key: null, environment: "usgovernment" }), { TF_WORKSPACE: "blue", ARM_SAS_TOKEN: "sv=x&sig=y" })).toMatchObject({ key: "app.tfstateenv:blue", endpoint: "https://acme.blob.core.usgovcloudapi.net", auth: { sas: "sv=x&sig=y" } });
|
|
77
|
+
const oidc = { ARM_TENANT_ID: "tenant", ARM_CLIENT_ID: "client", ARM_OIDC_TOKEN_FILE_PATH: "/t" };
|
|
78
|
+
expect(stateObject(initialised("azurerm", { ...AZ, access_key: null, snapshot: true }), oidc)).toMatchObject({ snapshot: true, auth: { oidc: { tenantId: "tenant", clientId: "client", tokenFile: "/t", authority: "https://login.microsoftonline.com" } } });
|
|
79
|
+
expect(stateObject(initialised("azurerm", { ...AZ, access_key: null }), {})).toMatchObject({ refused: "credentials" });
|
|
80
|
+
expect(stateObject(initialised("azurerm", { key: "k" }), {})).toMatchObject({ refused: "config", reason: expect.stringContaining("storage_account_name, container_name or key") });
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("a state chant does not lock", () => {
|
|
85
|
+
it("is an s3 backend without use_lockfile, named as such", () => {
|
|
86
|
+
for (const use_lockfile of [undefined, false]) {
|
|
87
|
+
const o = stateObject(initialised("s3", { ...S3_CONFIG, use_lockfile }), ENV);
|
|
88
|
+
expect(o).toMatchObject({ backend: "s3", refused: "no-lockfile", reason: expect.stringContaining("use_lockfile = true is unset") });
|
|
89
|
+
expect(stateLocation(o)).toBeUndefined();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("is a root with a cloud block, in .tf or .tf.json code, or one init recorded as cloud", () => {
|
|
94
|
+
const hcl = write(tmp(), { "main.tf": 'terraform {\n cloud {\n organization = "acme"\n }\n}\n' });
|
|
95
|
+
expect(stateObject(hcl, ENV)).toMatchObject({ refused: "cloud", reason: expect.stringContaining("cloud block") });
|
|
96
|
+
const json = write(tmp(), { "cdk.tf.json": JSON.stringify({ terraform: [{ required_version: ">= 1.10" }, { cloud: { organization: "acme" } }] }) });
|
|
97
|
+
expect(stateObject(json, ENV)).toMatchObject({ refused: "cloud" });
|
|
98
|
+
expect(stateObject(initialised("cloud", { organization: "acme" }), ENV)).toMatchObject({ backend: "cloud", refused: "cloud" });
|
|
99
|
+
// A cloud block added after init recorded s3: the code and the record disagree, and the refusal names both (chant#4478).
|
|
100
|
+
expect(stateObject(initialised("s3", S3_CONFIG, { "main.tf": "terraform {\n cloud {}\n}\n" }), ENV)).toMatchObject({
|
|
101
|
+
refused: "disagree",
|
|
102
|
+
reason: expect.stringContaining("names the cloud backend and init recorded s3"),
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("is any other backend, named by its type", () => {
|
|
107
|
+
for (const type of ["pg", "kubernetes", "consul", "http", "remote", "cos"]) {
|
|
108
|
+
const o = stateObject(initialised(type, {}), ENV);
|
|
109
|
+
expect(isRefused(o)).toBe(true);
|
|
110
|
+
expect(o).toMatchObject({ backend: type, refused: "backend", reason: expect.stringContaining(`backend is ${type}`) });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("is an s3 backend without its bucket or key, or without credentials", () => {
|
|
115
|
+
expect(stateObject(initialised("s3", { ...S3_CONFIG, key: null }), ENV)).toMatchObject({ refused: "config" });
|
|
116
|
+
expect(stateObject(initialised("s3", S3_CONFIG), {})).toMatchObject({ refused: "credentials", reason: expect.stringContaining("s3://acme-state") });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("the state module", () => {
|
|
121
|
+
it("is plain lexicon code: nothing in it imports from @intentius/chant/components", () => {
|
|
122
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
123
|
+
const sources = readdirSync(here).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts"));
|
|
124
|
+
expect(sources.length).toBeGreaterThan(5);
|
|
125
|
+
for (const f of sources) expect(readFileSync(join(here, f), "utf-8"), f).not.toMatch(/@intentius\/chant\/components/);
|
|
126
|
+
});
|
|
127
|
+
});
|