@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,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4618: the state operations run fixed argument lists on an injected exec,
|
|
3
|
+
* and the gated ones ask, spend and finish on an injected ledger, so no
|
|
4
|
+
* binary runs and no ledger is written.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import type { GatedRequest } from "@intentius/chant/lifecycle/gated-request";
|
|
8
|
+
import type { BinaryExec } from "./state/exec";
|
|
9
|
+
import {
|
|
10
|
+
applyPlanFile,
|
|
11
|
+
attachmentDigest,
|
|
12
|
+
forceUnlockState,
|
|
13
|
+
gatedStateOp,
|
|
14
|
+
planCopy,
|
|
15
|
+
planStateCheck,
|
|
16
|
+
pushStateFile,
|
|
17
|
+
requestedAt,
|
|
18
|
+
StateOpError,
|
|
19
|
+
STATE_OPS,
|
|
20
|
+
type GatedRequestLedger,
|
|
21
|
+
} from "./state-ops";
|
|
22
|
+
|
|
23
|
+
const PLAN = { resource_changes: [{ address: "aws_s3_bucket.logs", mode: "managed", change: { actions: ["create"], before: null } }] };
|
|
24
|
+
|
|
25
|
+
function recorder(fail?: string): { exec: BinaryExec; calls: string[][] } {
|
|
26
|
+
const calls: string[][] = [];
|
|
27
|
+
const exec: BinaryExec = async (_binary, args) => {
|
|
28
|
+
calls.push(args);
|
|
29
|
+
if (fail && args[0] === fail) return { code: 1, stdout: "", out: "Error: it broke" };
|
|
30
|
+
if (args[0] === "show") return { code: 0, stdout: JSON.stringify(PLAN), out: "" };
|
|
31
|
+
if (args[0] === "plan") return { code: 0, stdout: "", out: "Plan: 1 to add, 0 to change, 0 to destroy." };
|
|
32
|
+
return { code: 0, stdout: "", out: "" };
|
|
33
|
+
};
|
|
34
|
+
return { exec, calls };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const root = (exec: BinaryExec) => ({ root: "net", binary: "terraform", dir: "/repo/net", env: {}, exec });
|
|
38
|
+
|
|
39
|
+
describe("binary runs", () => {
|
|
40
|
+
test("force-unlock takes the lock ID and nothing else", async () => {
|
|
41
|
+
const { exec, calls } = recorder();
|
|
42
|
+
await forceUnlockState({ ...root(exec), id: "a1b2" });
|
|
43
|
+
expect(calls).toEqual([["force-unlock", "-force", "-no-color", "a1b2"]]);
|
|
44
|
+
await expect(forceUnlockState({ ...root(exec), id: "-help" })).rejects.toThrow(/not a lock ID/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("a failed run throws with the binary's last lines", async () => {
|
|
48
|
+
const { exec } = recorder("force-unlock");
|
|
49
|
+
await expect(forceUnlockState({ ...root(exec), id: "a1b2" })).rejects.toBeInstanceOf(StateOpError);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("state push passes -lock=false and -force only when asked", async () => {
|
|
53
|
+
const { exec, calls } = recorder();
|
|
54
|
+
await pushStateFile({ ...root(exec), file: "/w/net.tfstate" });
|
|
55
|
+
await pushStateFile({ ...root(exec), file: "/w/net.tfstate", lock: false });
|
|
56
|
+
await pushStateFile({ ...root(exec), file: "/w/net.tfstate", force: true });
|
|
57
|
+
expect(calls).toEqual([
|
|
58
|
+
["state", "push", "/w/net.tfstate"],
|
|
59
|
+
["state", "push", "-lock=false", "/w/net.tfstate"],
|
|
60
|
+
["state", "push", "-force", "/w/net.tfstate"],
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a migration's check plans with -lock=false while the caller holds the lock, and reads the plan", async () => {
|
|
65
|
+
const { exec, calls } = recorder();
|
|
66
|
+
const read = await planStateCheck({ ...root(exec), planFile: "/w/0.tfplan", lock: false });
|
|
67
|
+
expect(calls[0]).toEqual(["plan", "-input=false", "-no-color", "-lock=false", "-out=/w/0.tfplan"]);
|
|
68
|
+
expect(read.json).toEqual(PLAN);
|
|
69
|
+
expect(read.summary).toBe("Plan: 1 to add, 0 to change, 0 to destroy.");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a copy inits under its own key, plans a destroy with the lock wait, and applies only the saved plan", async () => {
|
|
73
|
+
const { exec, calls } = recorder();
|
|
74
|
+
const read = await planCopy({ ...root(exec), backendConfig: { key: "net/terraform.tfstate-pr-7" }, planFile: "/w/0.tfplan", destroy: true });
|
|
75
|
+
expect(calls[0]).toEqual(["init", "-input=false", "-no-color", "-reconfigure", "-backend-config=key=net/terraform.tfstate-pr-7"]);
|
|
76
|
+
expect(calls[1]).toEqual(["plan", "-input=false", "-no-color", "-lock-timeout=5m", "-destroy", "-out=/w/0.tfplan"]);
|
|
77
|
+
expect(read.part.member.member).toBe("net");
|
|
78
|
+
expect(read.planDigest).not.toBe("");
|
|
79
|
+
await applyPlanFile({ ...root(exec), planFile: "/w/0.tfplan", lockTimeout: false });
|
|
80
|
+
expect(calls.at(-1)).toEqual(["apply", "-input=false", "-no-color", "/w/0.tfplan"]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a copy with no backend settings of its own is refused before anything runs", async () => {
|
|
84
|
+
const { exec, calls } = recorder();
|
|
85
|
+
await expect(planCopy({ ...root(exec), backendConfig: {}, planFile: "/w/0.tfplan" })).rejects.toThrow(/its own state/);
|
|
86
|
+
expect(calls).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/** A ledger in memory: requests wait until `approve` is called. */
|
|
91
|
+
function fakeLedger(): GatedRequestLedger & { approve(by: string): void; writes: string[] } {
|
|
92
|
+
let approvedBy: string | undefined;
|
|
93
|
+
const writes: string[] = [];
|
|
94
|
+
const asked: GatedRequest[] = [];
|
|
95
|
+
let spent = false;
|
|
96
|
+
return {
|
|
97
|
+
writes,
|
|
98
|
+
approve(by: string) {
|
|
99
|
+
approvedBy = by;
|
|
100
|
+
},
|
|
101
|
+
async requestGate(input) {
|
|
102
|
+
asked.push(input.request);
|
|
103
|
+
const digest = `sha256:${input.request.subject}-${input.request.at}`;
|
|
104
|
+
if (spent) return { status: "spent", digest, request: input.request };
|
|
105
|
+
if (!approvedBy) {
|
|
106
|
+
writes.push("pending");
|
|
107
|
+
return { status: "waiting", digest, request: input.request, command: `chant approve ${input.op} ${input.request.subject} --plan ${digest}` };
|
|
108
|
+
}
|
|
109
|
+
return { status: "approved", digest, request: input.request, approval: { version: 1, kind: "resolution", op: input.op, gate: input.request.subject, resolvedBy: approvedBy, timestamp: "2026-10-27T10:00:00Z" } as never };
|
|
110
|
+
},
|
|
111
|
+
async spendGatedRequest(input) {
|
|
112
|
+
spent = true;
|
|
113
|
+
writes.push("applied");
|
|
114
|
+
return { version: 1, kind: "applied", op: input.op, gate: input.gate, planDigest: input.digest, approvedAt: "", approvedBy: "", timestamp: "2026-10-27T11:00:00Z" } as never;
|
|
115
|
+
},
|
|
116
|
+
async finishGatedRequest(input) {
|
|
117
|
+
writes.push(`finished:${input.result}:${JSON.stringify(input.detail ?? {})}`);
|
|
118
|
+
return { record: {} as never, pushed: true };
|
|
119
|
+
},
|
|
120
|
+
async readGatedRequests() {
|
|
121
|
+
return asked.map((request) => ({ op: STATE_OPS.unlock, gate: request.subject, digest: "", request, pending: { expiresAt: "2099-01-01T00:00:00Z" } as never, approvals: [] }));
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
describe("gatedStateOp", () => {
|
|
127
|
+
const input = (ledger: GatedRequestLedger, now: string) => ({ op: STATE_OPS.unlock, request: { kind: "unlock", subject: "net", by: "ana", fields: { id: "a1b2", location: "s3://b/net.tflock" } }, rules: { mode: "ledger" as const }, ledger, now });
|
|
128
|
+
|
|
129
|
+
test("waits, then acts once under the approval, with the same request time as the first ask", async () => {
|
|
130
|
+
const ledger = fakeLedger();
|
|
131
|
+
let acted = 0;
|
|
132
|
+
const act = async () => {
|
|
133
|
+
acted++;
|
|
134
|
+
return { output: "released", detail: { lock: "a1b2" } };
|
|
135
|
+
};
|
|
136
|
+
const first = await gatedStateOp(input(ledger, "2026-10-27T09:00:00Z"), act);
|
|
137
|
+
expect(first.status).toBe("waiting");
|
|
138
|
+
ledger.approve("bo");
|
|
139
|
+
const second = await gatedStateOp(input(ledger, "2026-10-27T12:00:00Z"), act);
|
|
140
|
+
expect(second.status).toBe("done");
|
|
141
|
+
expect(second.request.at).toBe("2026-10-27T09:00:00Z");
|
|
142
|
+
expect(acted).toBe(1);
|
|
143
|
+
expect(ledger.writes).toEqual(["pending", "applied", 'finished:applied:{"lock":"a1b2"}']);
|
|
144
|
+
const third = await gatedStateOp(input(ledger, "2026-10-27T13:00:00Z"), act);
|
|
145
|
+
expect(third.status).toBe("spent");
|
|
146
|
+
expect(acted).toBe(1);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("a last check that refuses leaves the approval unspent", async () => {
|
|
150
|
+
const ledger = fakeLedger();
|
|
151
|
+
ledger.approve("bo");
|
|
152
|
+
const held = await gatedStateOp({ ...input(ledger, "2026-10-27T09:00:00Z"), beforeSpend: async () => "a run that may hold the lock is alive" }, async () => ({ output: 1 }));
|
|
153
|
+
expect(held.status).toBe("held");
|
|
154
|
+
expect(ledger.writes).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("a failed act is recorded as failed", async () => {
|
|
158
|
+
const ledger = fakeLedger();
|
|
159
|
+
ledger.approve("bo");
|
|
160
|
+
const failed = await gatedStateOp(input(ledger, "2026-10-27T09:00:00Z"), async () => {
|
|
161
|
+
throw new Error("force-unlock failed");
|
|
162
|
+
});
|
|
163
|
+
expect(failed.status).toBe("failed");
|
|
164
|
+
expect(ledger.writes.at(-1)).toBe('finished:failed:{"error":"force-unlock failed"}');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("a request with an attachment keeps its time while the proof is the same, and asks anew when it changes", async () => {
|
|
168
|
+
const ledger = fakeLedger();
|
|
169
|
+
const ask = (now: string, proof: string) => gatedStateOp({ ...input(ledger, now), attachment: proof }, async () => ({ output: 0 }));
|
|
170
|
+
const first = await ask("2026-10-27T09:00:00Z", '{"roots":["net"]}');
|
|
171
|
+
expect(first.request.fields.proof).toBe(attachmentDigest('{"roots":["net"]}'));
|
|
172
|
+
expect((await ask("2026-10-27T10:00:00Z", '{"roots":["net"]}')).request.at).toBe("2026-10-27T09:00:00Z");
|
|
173
|
+
expect((await ask("2026-10-27T11:00:00Z", '{"roots":["net","app"]}')).request.at).toBe("2026-10-27T11:00:00Z");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("requestedAt asks anew when the fields differ", async () => {
|
|
177
|
+
const ledger = fakeLedger();
|
|
178
|
+
await gatedStateOp(input(ledger, "2026-10-27T09:00:00Z"), async () => ({ output: 0 }));
|
|
179
|
+
const other = await requestedAt(STATE_OPS.unlock, { kind: "unlock", subject: "net", by: "ana", fields: { id: "c3d4", location: "s3://b/net.tflock" } }, { now: "2026-10-27T12:00:00Z", ledger });
|
|
180
|
+
expect(other).toBe("2026-10-27T12:00:00Z");
|
|
181
|
+
});
|
|
182
|
+
});
|
package/src/state-ops.ts
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The binary runs outside op waves (#4618): the state-lock release, a state
|
|
3
|
+
* migration's writes and an ephemeral copy's plan and apply, as fixed
|
|
4
|
+
* operations of the terraform lexicon, with the gate and the records of
|
|
5
|
+
* #4551 around the ones that change state on their own.
|
|
6
|
+
*
|
|
7
|
+
* Each operation runs a fixed argument list. A caller names the root, the
|
|
8
|
+
* binary and what the operation acts on (a lock ID, a state file, a key
|
|
9
|
+
* suffix's backend settings), never extra arguments, so a product cannot
|
|
10
|
+
* turn an unlock into some other command.
|
|
11
|
+
*
|
|
12
|
+
* - `forceUnlockState` releases one lock by its ID with `force-unlock
|
|
13
|
+
* -force`. It changes state on its own, so it runs under a gated request:
|
|
14
|
+
* `gatedStateOp` asks for the request (`requestGate`), spends the approval
|
|
15
|
+
* (`spendGatedRequest`, pushed before anything runs) and records how it
|
|
16
|
+
* ended (`finishGatedRequest`, with the product's flat facts in `detail`).
|
|
17
|
+
* - `pushStateFile` writes one state file with `state push`, and
|
|
18
|
+
* `planStateCheck` plans a root against its state (`-lock=false` while the
|
|
19
|
+
* caller holds the lock itself) and reads the plan's JSON. A standalone
|
|
20
|
+
* migration's writes run under one gated request whose attachment is the
|
|
21
|
+
* proof, so the digest binds it. A migration inside a wave keeps the
|
|
22
|
+
* wave's hold hook, which chant decides already.
|
|
23
|
+
* - `planCopy` initialises a root under the backend settings of its copy
|
|
24
|
+
* (`init -reconfigure -backend-config=<k>=<v>`), plans it, a destroy too,
|
|
25
|
+
* and reads the plan as a change-set part; `applyPlanFile` applies a saved
|
|
26
|
+
* plan. A copy waits at chant's `pr-env` gate, which
|
|
27
|
+
* `components/pr-env-run` decides and records (#4551 gap 4), so it takes
|
|
28
|
+
* no request of its own.
|
|
29
|
+
*
|
|
30
|
+
* The capabilities at the end (`terraform-state-unlock`,
|
|
31
|
+
* `terraform-state-push`, `terraform-copy`) put the same operations in the
|
|
32
|
+
* plugin's registry, so a step names them by kind.
|
|
33
|
+
*
|
|
34
|
+
* The exec is injectable (`./state/exec.ts`), and so is the gated-request
|
|
35
|
+
* ledger, so a test runs no binary and writes no ledger.
|
|
36
|
+
*/
|
|
37
|
+
import { createHash } from "node:crypto";
|
|
38
|
+
import { join } from "node:path";
|
|
39
|
+
import type { Capability, CapabilityPlan } from "@intentius/chant/components/capability";
|
|
40
|
+
import type { ChangeSetPart } from "@intentius/chant/change-set";
|
|
41
|
+
import { CHANT_EXIT } from "@intentius/chant/exit-codes";
|
|
42
|
+
import {
|
|
43
|
+
finishGatedRequest,
|
|
44
|
+
gatedRequestDigest,
|
|
45
|
+
readGatedRequests,
|
|
46
|
+
requestGate,
|
|
47
|
+
spendGatedRequest,
|
|
48
|
+
type GatedRequest,
|
|
49
|
+
type GatedRequestRules,
|
|
50
|
+
type RequestGateInput,
|
|
51
|
+
type RequestGateResult,
|
|
52
|
+
} from "@intentius/chant/lifecycle/gated-request";
|
|
53
|
+
import { plannerForBinary, terraformChangeSetPart } from "./change-set";
|
|
54
|
+
import { DEFAULT_LOCK_TIMEOUT, lockTimeoutArgs } from "./lock-timeout";
|
|
55
|
+
import { lastLines, runBinary, type BinaryExec } from "./state/exec";
|
|
56
|
+
|
|
57
|
+
/** The ops the gated requests of this module wait at, one per kind, as terragucci named them. */
|
|
58
|
+
export const STATE_OPS = {
|
|
59
|
+
unlock: "tf-unlock",
|
|
60
|
+
migrate: "tf-migrate",
|
|
61
|
+
export: "tf-state-export",
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
/** The request kinds of #4551's design. */
|
|
65
|
+
export const STATE_OP_KINDS = {
|
|
66
|
+
unlock: "unlock",
|
|
67
|
+
migrate: "migrate",
|
|
68
|
+
export: "state-export",
|
|
69
|
+
} as const;
|
|
70
|
+
|
|
71
|
+
/** A binary run that exited non-zero, with the tail of its output. */
|
|
72
|
+
export class StateOpError extends Error {
|
|
73
|
+
constructor(
|
|
74
|
+
readonly root: string,
|
|
75
|
+
readonly command: string,
|
|
76
|
+
readonly code: number,
|
|
77
|
+
readonly tail: string,
|
|
78
|
+
) {
|
|
79
|
+
super(`${root}: ${command} failed (exit ${code})${tail ? `:\n${tail}` : ""}`);
|
|
80
|
+
this.name = "StateOpError";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A gated operation stopped for a person: its request waits for an approval. The command that ran it exits 3. */
|
|
85
|
+
export class StateOpWaitingError extends Error {
|
|
86
|
+
readonly exitCode = CHANT_EXIT.waiting;
|
|
87
|
+
constructor(
|
|
88
|
+
readonly op: string,
|
|
89
|
+
readonly subject: string,
|
|
90
|
+
readonly digest: string,
|
|
91
|
+
readonly command: string,
|
|
92
|
+
) {
|
|
93
|
+
super(`${op} ${subject} waits for an approval of ${digest}: ${command}`);
|
|
94
|
+
this.name = "StateOpWaitingError";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Where and how the binary runs for one root. */
|
|
99
|
+
export interface RootRun {
|
|
100
|
+
/** The root's name, as messages and records name it. */
|
|
101
|
+
root: string;
|
|
102
|
+
binary: string;
|
|
103
|
+
/** The root's directory. */
|
|
104
|
+
dir: string;
|
|
105
|
+
env?: NodeJS.ProcessEnv;
|
|
106
|
+
exec?: BinaryExec;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const ran = async (r: RootRun, args: string[], label: string): Promise<{ stdout: string; out: string }> => {
|
|
110
|
+
const exec = r.exec ?? runBinary;
|
|
111
|
+
const done = await exec(r.binary, args, r.dir, r.env ?? process.env);
|
|
112
|
+
if (done.code !== 0) throw new StateOpError(r.root, `${r.binary} ${label}`, done.code, lastLines(done.out, 10));
|
|
113
|
+
return done;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ── the binary runs ─────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/** `init -input=false -no-color`, with `-reconfigure` and `-backend-config` pairs when given. */
|
|
119
|
+
export async function initRoot(r: RootRun & { backendConfig?: Record<string, string>; reconfigure?: boolean }): Promise<void> {
|
|
120
|
+
const configs = Object.entries(r.backendConfig ?? {}).map(([k, v]) => `-backend-config=${k}=${v}`);
|
|
121
|
+
await ran(r, ["init", "-input=false", "-no-color", ...(r.reconfigure || configs.length > 0 ? ["-reconfigure"] : []), ...configs], "init");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Release the state lock with this ID: `force-unlock -force -no-color <id>`.
|
|
126
|
+
* Run it under {@link gatedStateOp}; the binary releases whichever lock has
|
|
127
|
+
* the ID, so the approval binds the ID and the lock's location.
|
|
128
|
+
*/
|
|
129
|
+
export async function forceUnlockState(r: RootRun & { id: string }): Promise<void> {
|
|
130
|
+
if (!r.id || r.id.startsWith("-")) throw new Error(`${r.root}: "${r.id}" is not a lock ID`);
|
|
131
|
+
await ran(r, ["force-unlock", "-force", "-no-color", r.id], `force-unlock ${r.id}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Write a state file with `state push`. `lock: false` passes `-lock=false`,
|
|
136
|
+
* for a caller that holds the state's lock itself; `force` passes `-force`,
|
|
137
|
+
* for a push over an empty state of another lineage the caller checked.
|
|
138
|
+
*/
|
|
139
|
+
export async function pushStateFile(r: RootRun & { file: string; lock?: boolean; force?: boolean }): Promise<void> {
|
|
140
|
+
if (r.file.startsWith("-")) throw new Error(`${r.root}: "${r.file}" is not a state file`);
|
|
141
|
+
await ran(r, ["state", "push", ...(r.lock === false ? ["-lock=false"] : []), ...(r.force ? ["-force"] : []), r.file], "state push");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A plan read back: its JSON, and the binary's summary line. */
|
|
145
|
+
export interface PlanRead {
|
|
146
|
+
planFile: string;
|
|
147
|
+
json: unknown;
|
|
148
|
+
summary: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function planAndShow(r: RootRun, planArgs: string[], planFile: string): Promise<PlanRead> {
|
|
152
|
+
const plan = await ran(r, ["plan", "-input=false", "-no-color", ...planArgs, `-out=${planFile}`], "plan");
|
|
153
|
+
const show = await ran(r, ["show", "-json", planFile], "show -json");
|
|
154
|
+
let json: unknown;
|
|
155
|
+
try {
|
|
156
|
+
json = JSON.parse(show.stdout);
|
|
157
|
+
} catch {
|
|
158
|
+
throw new StateOpError(r.root, `${r.binary} show -json`, 0, `printed no plan: ${lastLines(show.out, 10)}`);
|
|
159
|
+
}
|
|
160
|
+
return { planFile, json, summary: plan.out.match(/Plan: .*|No changes\..*/)?.[0] ?? "planned" };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Plan a root against its state and read the plan: the check a migration
|
|
165
|
+
* makes before and after it writes. `lock: false` passes `-lock=false`, for a
|
|
166
|
+
* caller that holds the lock itself.
|
|
167
|
+
*/
|
|
168
|
+
export function planStateCheck(r: RootRun & { planFile: string; lock?: boolean }): Promise<PlanRead> {
|
|
169
|
+
return planAndShow(r, r.lock === false ? ["-lock=false"] : [], r.planFile);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A copy's plan: the plan read back, and its change-set part with the member's plan digest. */
|
|
173
|
+
export interface CopyPlanRead extends PlanRead {
|
|
174
|
+
part: ChangeSetPart;
|
|
175
|
+
planDigest: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Initialise a root under its copy's backend settings and plan it, a destroy
|
|
180
|
+
* with `destroy`. `backendConfig` is what makes it the copy (a state key with
|
|
181
|
+
* the copy's suffix), so it is required and never empty: without it the plan
|
|
182
|
+
* would be of the root's own state. Pass `init: false` for a root another
|
|
183
|
+
* tool initialised (a Terragrunt unit prepared with the suffix set).
|
|
184
|
+
*/
|
|
185
|
+
export async function planCopy(
|
|
186
|
+
r: RootRun & { backendConfig: Record<string, string>; planFile: string; destroy?: boolean; lockTimeout?: string | false; init?: boolean },
|
|
187
|
+
): Promise<CopyPlanRead> {
|
|
188
|
+
if (r.init !== false) {
|
|
189
|
+
if (Object.keys(r.backendConfig).length === 0) throw new Error(`${r.root}: a copy needs the backend settings of its own state`);
|
|
190
|
+
await initRoot({ ...r, reconfigure: true });
|
|
191
|
+
}
|
|
192
|
+
const read = await planAndShow(r, [...lockTimeoutArgs("plan", r.lockTimeout ?? DEFAULT_LOCK_TIMEOUT, r.env ?? process.env), ...(r.destroy ? ["-destroy"] : [])], r.planFile);
|
|
193
|
+
const part = terraformChangeSetPart({ member: r.root, plan: read.json, planner: plannerForBinary(r.binary) });
|
|
194
|
+
return { ...read, part, planDigest: part.member.planDigest ?? "" };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Apply a saved plan, and nothing else: `apply -input=false -no-color <plan>`. */
|
|
198
|
+
export async function applyPlanFile(r: RootRun & { planFile: string; lockTimeout?: string | false }): Promise<void> {
|
|
199
|
+
await ran(r, ["apply", "-input=false", "-no-color", ...lockTimeoutArgs("apply", r.lockTimeout ?? DEFAULT_LOCK_TIMEOUT, r.env ?? process.env), r.planFile], "apply");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── the gate around them ────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/** The gated-request ledger, injectable for tests. Default chant's on `chant/lifecycle`. */
|
|
205
|
+
export interface GatedRequestLedger {
|
|
206
|
+
requestGate: typeof requestGate;
|
|
207
|
+
spendGatedRequest: typeof spendGatedRequest;
|
|
208
|
+
finishGatedRequest: typeof finishGatedRequest;
|
|
209
|
+
readGatedRequests: typeof readGatedRequests;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const chantLedger: GatedRequestLedger = { requestGate, spendGatedRequest, finishGatedRequest, readGatedRequests };
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The time a request was first asked, so asking again binds the same
|
|
216
|
+
* digest: chant's digest covers `at`, and a run that asked anew each time
|
|
217
|
+
* would never meet the approval of the last one. The newest standing,
|
|
218
|
+
* unspent request with the same kind, subject, requester and fields gives
|
|
219
|
+
* its `at`; with none, `now`.
|
|
220
|
+
*/
|
|
221
|
+
export async function requestedAt(
|
|
222
|
+
op: string,
|
|
223
|
+
request: Omit<GatedRequest, "at">,
|
|
224
|
+
options: { now?: string; cwd?: string; ledger?: Pick<GatedRequestLedger, "readGatedRequests"> } = {},
|
|
225
|
+
): Promise<string> {
|
|
226
|
+
const now = options.now ?? new Date().toISOString();
|
|
227
|
+
const read = await (options.ledger ?? chantLedger).readGatedRequests(op, options.cwd ? { cwd: options.cwd } : undefined).catch(() => []);
|
|
228
|
+
const same = JSON.stringify(Object.entries(request.fields).sort());
|
|
229
|
+
const standing = read
|
|
230
|
+
.filter((g) => !g.applied && g.request.kind === request.kind && g.request.subject === request.subject && g.request.by === request.by)
|
|
231
|
+
.filter((g) => JSON.stringify(Object.entries(g.request.fields).sort()) === same)
|
|
232
|
+
.filter((g) => !g.pending.expiresAt || g.pending.expiresAt > now)
|
|
233
|
+
.at(-1);
|
|
234
|
+
return standing?.request.at ?? now;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The digest `requestGate` puts in `fields.proof` for an attachment. */
|
|
238
|
+
export const attachmentDigest = (attachment: string): string => `sha256:${createHash("sha256").update(attachment).digest("hex")}`;
|
|
239
|
+
|
|
240
|
+
export interface GatedStateOpInput {
|
|
241
|
+
op: string;
|
|
242
|
+
/** The request, less `at`, which {@link requestedAt} fills. */
|
|
243
|
+
request: Omit<GatedRequest, "at"> & { at?: string };
|
|
244
|
+
rules: GatedRequestRules;
|
|
245
|
+
description?: string;
|
|
246
|
+
expiry?: string;
|
|
247
|
+
base?: string;
|
|
248
|
+
/** A document the request carries, such as a migration's proof; its digest joins the request. */
|
|
249
|
+
attachment?: string;
|
|
250
|
+
cwd?: string;
|
|
251
|
+
now?: string;
|
|
252
|
+
runId?: string;
|
|
253
|
+
commit?: string;
|
|
254
|
+
ledger?: GatedRequestLedger;
|
|
255
|
+
gates?: RequestGateInput["gates"];
|
|
256
|
+
/**
|
|
257
|
+
* A last check just before the spend, for a fact that can change while the
|
|
258
|
+
* request waited (a run that may hold the lock came back). A string refuses
|
|
259
|
+
* the operation with that reason, and the approval is not spent.
|
|
260
|
+
*/
|
|
261
|
+
beforeSpend?: () => Promise<string | undefined>;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** How a gated operation ended. */
|
|
265
|
+
export type GatedStateOpResult<T> =
|
|
266
|
+
| Exclude<RequestGateResult, { status: "approved" }>
|
|
267
|
+
| { status: "held"; digest: string; request: GatedRequest; reason: string }
|
|
268
|
+
| { status: "done"; digest: string; request: GatedRequest; approvedBy: string; output: T; recorded: boolean }
|
|
269
|
+
| { status: "failed"; digest: string; request: GatedRequest; approvedBy: string; error: Error; recorded: boolean };
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Run `act` once its request is approved: ask (or read) the request, spend
|
|
273
|
+
* the approval, act, and record how it ended. `act` returns its output and
|
|
274
|
+
* the flat facts that go on the finished line. A spend that cannot be pushed
|
|
275
|
+
* throws before `act` runs, so no approval acts twice. A finished line that
|
|
276
|
+
* cannot be written is reported as `recorded: false`; the act already ran.
|
|
277
|
+
*/
|
|
278
|
+
export async function gatedStateOp<T>(
|
|
279
|
+
input: GatedStateOpInput,
|
|
280
|
+
act: (approved: { digest: string; request: GatedRequest; approvedBy: string }) => Promise<{ output: T; detail?: Record<string, string | number | boolean> }>,
|
|
281
|
+
): Promise<GatedStateOpResult<T>> {
|
|
282
|
+
const ledger = input.ledger ?? chantLedger;
|
|
283
|
+
// The proof's digest is a field of the stored request, so the standing request is matched with it.
|
|
284
|
+
const fields = input.attachment !== undefined ? { ...input.request.fields, proof: attachmentDigest(input.attachment) } : input.request.fields;
|
|
285
|
+
const at = input.request.at ?? (await requestedAt(input.op, { ...input.request, fields }, { ...(input.now ? { now: input.now } : {}), ...(input.cwd ? { cwd: input.cwd } : {}), ledger }));
|
|
286
|
+
const request: GatedRequest = { kind: input.request.kind, subject: input.request.subject, by: input.request.by, at, fields };
|
|
287
|
+
const asked = await ledger.requestGate({
|
|
288
|
+
op: input.op,
|
|
289
|
+
request,
|
|
290
|
+
rules: input.rules,
|
|
291
|
+
...(input.description ? { description: input.description } : {}),
|
|
292
|
+
...(input.expiry ? { expiry: input.expiry } : {}),
|
|
293
|
+
...(input.base ? { base: input.base } : {}),
|
|
294
|
+
...(input.attachment !== undefined ? { attachment: input.attachment } : {}),
|
|
295
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
296
|
+
...(input.now ? { now: input.now } : {}),
|
|
297
|
+
...(input.gates ? { gates: input.gates } : {}),
|
|
298
|
+
});
|
|
299
|
+
if (asked.status !== "approved") return asked;
|
|
300
|
+
const { digest } = asked;
|
|
301
|
+
const approvedBy = asked.approval.resolvedBy;
|
|
302
|
+
const held = await input.beforeSpend?.();
|
|
303
|
+
if (held) return { status: "held", digest, request: asked.request, reason: held };
|
|
304
|
+
const spent = await ledger.spendGatedRequest({
|
|
305
|
+
op: input.op,
|
|
306
|
+
gate: request.subject,
|
|
307
|
+
digest,
|
|
308
|
+
approval: asked.approval,
|
|
309
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
310
|
+
...(input.commit ? { commit: input.commit } : {}),
|
|
311
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
312
|
+
...(input.now ? { now: input.now } : {}),
|
|
313
|
+
});
|
|
314
|
+
const finish = async (result: "applied" | "failed", detail?: Record<string, string | number | boolean>): Promise<boolean> => {
|
|
315
|
+
try {
|
|
316
|
+
const f = await ledger.finishGatedRequest({ op: input.op, gate: request.subject, digest, applied: spent.timestamp, result, ...(detail ? { detail } : {}), ...(input.cwd ? { cwd: input.cwd } : {}) });
|
|
317
|
+
return f.pushed;
|
|
318
|
+
} catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
try {
|
|
323
|
+
const { output, detail } = await act({ digest, request: asked.request, approvedBy });
|
|
324
|
+
return { status: "done", digest, request: asked.request, approvedBy, output, recorded: await finish("applied", detail) };
|
|
325
|
+
} catch (e) {
|
|
326
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
327
|
+
return { status: "failed", digest, request: asked.request, approvedBy, error, recorded: await finish("failed", { error: error.message.slice(0, 400) }) };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** The digest a request binds, for a caller that prints it before it asks. */
|
|
332
|
+
export const stateOpDigest = (op: string, request: GatedRequest): string => gatedRequestDigest(op, request);
|
|
333
|
+
|
|
334
|
+
// ── capabilities ────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
/** The step input the gated capabilities share: the root and the request's rules. */
|
|
337
|
+
interface GatedRootInput {
|
|
338
|
+
root: string;
|
|
339
|
+
/** The project's directory, where the ledger is and roots are found. Default the process's. */
|
|
340
|
+
cwd?: string;
|
|
341
|
+
/** The root's directory. Default `root` under the project. */
|
|
342
|
+
dir?: string;
|
|
343
|
+
binary?: string;
|
|
344
|
+
/** Who asks. Default the CI actor, else the user. */
|
|
345
|
+
by?: string;
|
|
346
|
+
/** Which approvals count. Default the ledger's. */
|
|
347
|
+
approval?: GatedRequestRules["mode"];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** `terraform-state-unlock`: release one lock, by ID, under a gated request. */
|
|
351
|
+
export interface StateUnlockStepInput extends GatedRootInput {
|
|
352
|
+
id: string;
|
|
353
|
+
/** Where the lock is, as the record names it: the lock file, the lock object, the lease's blob. */
|
|
354
|
+
location: string;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** `terraform-state-push`: write state files to their roots under one gated request whose attachment is the proof. */
|
|
358
|
+
export interface StatePushStepInput extends GatedRootInput {
|
|
359
|
+
files: Array<{ root: string; dir?: string; file: string; lock?: boolean; force?: boolean }>;
|
|
360
|
+
/** The proof the approval binds: what the push writes and why it is safe. */
|
|
361
|
+
proof: string;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** `terraform-copy`: plan and apply a root's copy under its own backend settings. */
|
|
365
|
+
export interface CopyStepInput {
|
|
366
|
+
root: string;
|
|
367
|
+
cwd?: string;
|
|
368
|
+
dir?: string;
|
|
369
|
+
binary?: string;
|
|
370
|
+
backendConfig: Record<string, string>;
|
|
371
|
+
destroy?: boolean;
|
|
372
|
+
lockTimeout?: string | false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const actorOf = (env: NodeJS.ProcessEnv): string => env.GITHUB_ACTOR || env.GITLAB_USER_LOGIN || env.USER || env.USERNAME || "unknown";
|
|
376
|
+
|
|
377
|
+
const dirOf = (cwd: string | undefined, input: { root: string; dir?: string }): string => input.dir ?? join(cwd ?? process.cwd(), input.root);
|
|
378
|
+
|
|
379
|
+
async function gatedRun<T>(op: string, input: GatedRootInput, kind: string, fields: Record<string, string>, act: () => Promise<{ output: T; detail?: Record<string, string | number | boolean> }>, attachment?: string): Promise<T> {
|
|
380
|
+
const cwd = input.cwd ?? process.cwd();
|
|
381
|
+
const result = await gatedStateOp<T>(
|
|
382
|
+
{ op, request: { kind, subject: input.root, by: input.by ?? actorOf(process.env), fields }, rules: { mode: input.approval ?? "ledger" }, cwd, ...(attachment !== undefined ? { attachment } : {}) },
|
|
383
|
+
act,
|
|
384
|
+
);
|
|
385
|
+
if (result.status === "done") return result.output;
|
|
386
|
+
if (result.status === "failed") throw result.error;
|
|
387
|
+
if (result.status === "spent") throw new Error(`${op} ${input.root}: the request ${result.digest} applied already, and a request applies once`);
|
|
388
|
+
if (result.status === "held") throw new Error(`${op} ${input.root}: ${result.reason}`);
|
|
389
|
+
throw new StateOpWaitingError(op, input.root, result.digest, result.command);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** The plan a gated capability answers: the request's digest is computed when it is asked, so the plan binds the step's facts alone. */
|
|
393
|
+
const factsPlan = (member: string, facts: Record<string, string>): CapabilityPlan => ({ member, planDigest: stateOpDigest("plan", { kind: "facts", subject: member, by: "", at: "", fields: facts }) });
|
|
394
|
+
|
|
395
|
+
export const terraformStateUnlockCapability: Capability<StateUnlockStepInput, { root: string; id: string }> = {
|
|
396
|
+
kind: "terraform-state-unlock",
|
|
397
|
+
rollbackPolicy: "none-by-design",
|
|
398
|
+
targets: { refuse: "a lock release acts on one lock by its ID" },
|
|
399
|
+
async plan(_ctx, input) {
|
|
400
|
+
return factsPlan(input.root, { id: input.id, location: input.location });
|
|
401
|
+
},
|
|
402
|
+
async run(_ctx, input) {
|
|
403
|
+
const dir = dirOf(input.cwd, input);
|
|
404
|
+
const binary = input.binary ?? "terraform";
|
|
405
|
+
return gatedRun(STATE_OPS.unlock, input, STATE_OP_KINDS.unlock, { location: input.location, id: input.id }, async () => {
|
|
406
|
+
await initRoot({ root: input.root, binary, dir });
|
|
407
|
+
await forceUnlockState({ root: input.root, binary, dir, id: input.id });
|
|
408
|
+
return { output: { root: input.root, id: input.id }, detail: { lock: input.id, location: input.location } };
|
|
409
|
+
});
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
export const terraformStatePushCapability: Capability<StatePushStepInput, { roots: string[] }> = {
|
|
414
|
+
kind: "terraform-state-push",
|
|
415
|
+
rollbackPolicy: "needs-opt-out",
|
|
416
|
+
targets: { refuse: "a state push writes whole states" },
|
|
417
|
+
async plan(_ctx, input) {
|
|
418
|
+
return factsPlan(input.root, { roots: input.files.map((f) => f.root).join(",") });
|
|
419
|
+
},
|
|
420
|
+
async run(_ctx, input) {
|
|
421
|
+
const binary = input.binary ?? "terraform";
|
|
422
|
+
return gatedRun(
|
|
423
|
+
STATE_OPS.migrate,
|
|
424
|
+
input,
|
|
425
|
+
STATE_OP_KINDS.migrate,
|
|
426
|
+
{ roots: input.files.map((f) => f.root).join(",") },
|
|
427
|
+
async () => {
|
|
428
|
+
for (const f of input.files) await pushStateFile({ root: f.root, binary, dir: dirOf(input.cwd, f), file: f.file, ...(f.lock !== undefined ? { lock: f.lock } : {}), ...(f.force ? { force: true } : {}) });
|
|
429
|
+
return { output: { roots: input.files.map((f) => f.root) }, detail: { roots: input.files.length } };
|
|
430
|
+
},
|
|
431
|
+
input.proof,
|
|
432
|
+
);
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
/** The plan file a copy's plan leaves for its run. */
|
|
437
|
+
interface CopyArtifact {
|
|
438
|
+
planFile: string;
|
|
439
|
+
planDigest: string;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export const terraformCopyCapability: Capability<CopyStepInput, { root: string; planDigest: string }> = {
|
|
443
|
+
kind: "terraform-copy",
|
|
444
|
+
rollbackPolicy: "needs-opt-out",
|
|
445
|
+
targets: { refuse: "a copy plans the whole root" },
|
|
446
|
+
async plan(_ctx, input) {
|
|
447
|
+
const dir = dirOf(input.cwd, input);
|
|
448
|
+
const planFile = join(dir, input.destroy ? ".chant-copy-destroy.tfplan" : ".chant-copy.tfplan");
|
|
449
|
+
const read = await planCopy({ root: input.root, binary: input.binary ?? "terraform", dir, backendConfig: input.backendConfig, planFile, ...(input.destroy ? { destroy: true } : {}), ...(input.lockTimeout !== undefined ? { lockTimeout: input.lockTimeout } : {}) });
|
|
450
|
+
const artifact: CopyArtifact = { planFile, planDigest: read.planDigest };
|
|
451
|
+
return { member: input.root, planDigest: read.planDigest, artifact, changeSet: read.part };
|
|
452
|
+
},
|
|
453
|
+
async run(ctx, input) {
|
|
454
|
+
const kept = ctx.plans?.[input.root] as CopyArtifact | undefined;
|
|
455
|
+
const planned = kept && typeof kept.planFile === "string" ? kept : await terraformCopyCapability.plan!(ctx, input).then((p) => p.artifact as CopyArtifact);
|
|
456
|
+
await applyPlanFile({ root: input.root, binary: input.binary ?? "terraform", dir: dirOf(input.cwd, input), planFile: planned.planFile, ...(input.lockTimeout !== undefined ? { lockTimeout: input.lockTimeout } : {}) });
|
|
457
|
+
return { root: input.root, planDigest: planned.planDigest };
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
/** The state operations' capabilities, for the plugin's registry. */
|
|
462
|
+
export const terraformStateOpCapabilities = [terraformStateUnlockCapability, terraformStatePushCapability, terraformCopyCapability] as const;
|