@intentius/chant-lexicon-terraform 0.124.54 → 0.124.56
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 +7 -4
- 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/components/capability-plugin.d.ts +40 -6
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +174 -30
- package/dist/components/compose-group.d.ts +1 -1
- package/dist/components/compose-group.d.ts.map +1 -1
- package/dist/components/compose-group.js +1 -1
- 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/cone.d.ts +53 -0
- package/dist/cone.d.ts.map +1 -0
- package/dist/cone.js +119 -0
- 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/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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/integrity.json +7 -4
- 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/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/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/terraform.d.ts +29 -0
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activities/terraform.js +19 -4
- 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/plugin.d.ts.map +1 -1
- package/dist/plugin.js +5 -0
- 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/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/state/azure-blob.d.ts +8 -67
- package/dist/state/azure-blob.d.ts.map +1 -1
- package/dist/state/azure-blob.js +6 -199
- package/dist/state/gcs.d.ts +7 -61
- package/dist/state/gcs.d.ts.map +1 -1
- package/dist/state/gcs.js +5 -222
- package/dist/state/object-store.d.ts +4 -33
- package/dist/state/object-store.d.ts.map +1 -1
- package/dist/state/object-store.js +4 -17
- package/dist/state/s3.d.ts +11 -58
- package/dist/state/s3.d.ts.map +1 -1
- package/dist/state/s3.js +10 -161
- package/dist/state/store.js +2 -2
- 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/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 +19 -3
- package/src/__fixtures__/drift-record/README.md +5 -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__/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/ci-setup.ts +42 -0
- package/src/components/capability-plugin.test.ts +26 -14
- package/src/components/capability-plugin.ts +185 -31
- package/src/components/compose-group.ts +1 -1
- package/src/components/pr-loop-compose.tofu.e2e.test.ts +1 -1
- package/src/components/pr-loop.tofu.test.ts +1 -1
- 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/cone.ts +138 -0
- package/src/cost-hold.ts +29 -0
- package/src/describe-resources.engine-facts.test.ts +2 -3
- 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/index.ts +3 -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/lock-rows.test.ts +70 -0
- package/src/lock-rows.ts +82 -0
- package/src/lock-timeout.ts +34 -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/terraform.ts +46 -5
- 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/consumers.test.ts +57 -0
- package/src/pin/consumers.ts +95 -0
- package/src/plugin.ts +6 -0
- 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/state/azure-blob.ts +6 -221
- package/src/state/gcs.ts +5 -252
- package/src/state/object-store.ts +4 -32
- package/src/state/s3.ts +10 -182
- package/src/state/store.ts +2 -2
- package/src/state-ops.test.ts +182 -0
- package/src/state-ops.ts +462 -0
- package/src/state-reads.ts +116 -0
- package/src/used-sources.test.ts +75 -0
- package/src/used-sources.ts +204 -0
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* system and refuses with exit 3 when its fresh plan differs (choudoufu
|
|
23
23
|
* #878). Either refusal fails the step. choudoufu's refusal of a plan the
|
|
24
24
|
* wave handed `run` throws {@link TerraformPlanMovedError}, so the command
|
|
25
|
-
* that applied it exits
|
|
25
|
+
* that applied it exits 4, the table's moved code (#3997, #4553). terraform's and tofu's stale-plan error
|
|
26
26
|
* is not classified and exits 1.
|
|
27
27
|
*
|
|
28
28
|
* Run without a wave plan (`chant run --components`, or a fan-out with no
|
|
@@ -100,9 +100,31 @@
|
|
|
100
100
|
* read the way `plan` reads a root's (`terraformPlannedOutputs`). The member
|
|
101
101
|
* is not a `terraform.roots` entry of the reading project; its directory
|
|
102
102
|
* comes from the workspace declaration.
|
|
103
|
+
*
|
|
104
|
+
* ## Op waves (#4545)
|
|
105
|
+
*
|
|
106
|
+
* The step is what an op wave's `step` names to plan and apply each root
|
|
107
|
+
* through chant (`chant run wave`), so terragucci's waves and plywood's run
|
|
108
|
+
* the engine in chant. Three inputs come from terragucci's wave callbacks:
|
|
109
|
+
*
|
|
110
|
+
* - `lockTimeout` (default `5m`, `false` for none): `plan` and `apply` wait
|
|
111
|
+
* that long for another run's state lock, unless `TF_CLI_ARGS` sets
|
|
112
|
+
* `-lock-timeout` already (`../lock-timeout.ts`).
|
|
113
|
+
* - `cone`: a revision. A live choudoufu root plans the cone of the change
|
|
114
|
+
* since it (`plan -cone`, `../cone.ts`), and the cone's targets join the
|
|
115
|
+
* plan digest, so an approval of one cone counts for no other. Such a root
|
|
116
|
+
* plans with `plan`, never as a one-root set.
|
|
117
|
+
* - `reads`: vars read from another root's outputs, Atmos's
|
|
118
|
+
* `!terraform.state` (`../state-reads.ts`). An upstream with no such output
|
|
119
|
+
* yet makes the member a hole naming it, so the root is never planned on a
|
|
120
|
+
* stand-in.
|
|
121
|
+
*
|
|
122
|
+
* Every plan also writes its `show -json` beside the plan file and names it
|
|
123
|
+
* as `artifact.planJson`, the document a wave's cost hold prices
|
|
124
|
+
* (`../cost-hold.ts`).
|
|
103
125
|
*/
|
|
104
126
|
|
|
105
|
-
import { rmSync } from "node:fs";
|
|
127
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
106
128
|
import { dirname, join, resolve } from "node:path";
|
|
107
129
|
import type { Capability, DeployContext, CapabilityPlan, PlannedOutputs } from "@intentius/chant/components/capability";
|
|
108
130
|
import { loadChantConfigUpward } from "@intentius/chant/config";
|
|
@@ -127,13 +149,22 @@ import {
|
|
|
127
149
|
terraformOutputs,
|
|
128
150
|
terraformPlan,
|
|
129
151
|
} from "../op/activities/terraform";
|
|
152
|
+
import { terraformPlanMoved } from "../exit-codes";
|
|
130
153
|
import { terraformPlanShape } from "../plan-shape";
|
|
131
|
-
import { terraformChangeSetPart } from "../change-set";
|
|
154
|
+
import { plannerForBinary, terraformChangeSetPart } from "../change-set";
|
|
155
|
+
import { terraformDefaultLockGrain, terraformLockGrainRefusal, terraformLockRows, terraformLockRowsFromPart } from "../lock-rows";
|
|
156
|
+
import type { LockRow } from "@intentius/chant/lifecycle/lock-rows";
|
|
132
157
|
import type { TerraformConfig } from "../config";
|
|
133
158
|
import { rootTexts } from "../compose/hcl";
|
|
134
159
|
import { declaredVariables } from "../compose/root";
|
|
135
160
|
import { terraformComposeGroup } from "./compose-group";
|
|
136
161
|
import { markPlaceholderDriven, placeholderVars, waitingHole } from "./placeholder-preview";
|
|
162
|
+
import { terraformReleaseInputs } from "./release-inputs";
|
|
163
|
+
import { terraformTraceFacts } from "./trace-facts";
|
|
164
|
+
import { DEFAULT_LOCK_TIMEOUT, isLockTimeout } from "../lock-timeout";
|
|
165
|
+
import { conePlanDigest, type PlanCone } from "../cone";
|
|
166
|
+
import { fillStateReads, stateReadsProblems, waitingReadsReason, type TerraformStateRead } from "../state-reads";
|
|
167
|
+
import { terraformStateOpCapabilities } from "../state-ops";
|
|
137
168
|
|
|
138
169
|
/** The `terraform-apply` step's input. */
|
|
139
170
|
export interface TerraformApplyStepInput {
|
|
@@ -143,6 +174,14 @@ export interface TerraformApplyStepInput {
|
|
|
143
174
|
vars?: Record<string, unknown>;
|
|
144
175
|
/** Where the `chant.config.*` search starts. Default: the current directory. */
|
|
145
176
|
cwd?: string;
|
|
177
|
+
/** The root's files from here instead of its entry's `dir` (#4542): a promote under a release sets it to the recorded copy. */
|
|
178
|
+
dir?: string;
|
|
179
|
+
/** How long `plan` and `apply` wait for a state lock (#4545). Default `"5m"`; `false` passes no flag. */
|
|
180
|
+
lockTimeout?: string | false;
|
|
181
|
+
/** choudoufu live roots (#4545): plan the cone of the change since this revision. */
|
|
182
|
+
cone?: string;
|
|
183
|
+
/** Vars read from other roots' outputs (#4545), Atmos's `!terraform.state`. The step's `vars` win over them. */
|
|
184
|
+
reads?: TerraformStateRead[];
|
|
146
185
|
}
|
|
147
186
|
|
|
148
187
|
/** What `terraform-apply` returns. `outputs` is what a dependent's `stackOutput()` reads. */
|
|
@@ -159,9 +198,53 @@ export interface TerraformApplyStepOutput {
|
|
|
159
198
|
interface PlannedRoot {
|
|
160
199
|
planFile: string;
|
|
161
200
|
planDigest: string;
|
|
201
|
+
/** The plan's `show -json`, beside the plan file (#4545), for a cost hold to price. */
|
|
202
|
+
planJson?: string;
|
|
203
|
+
/** The cone the plan covered, when asked for one (#4545). */
|
|
204
|
+
cone?: PlanCone;
|
|
205
|
+
/** A choudoufu root's rows on the resource lock grain (#4552), from the plan's `show -json`. */
|
|
206
|
+
lockRows?: LockRow[];
|
|
162
207
|
}
|
|
163
208
|
|
|
164
|
-
|
|
209
|
+
/** The lock timeout a step's plan and apply pass (#4545). */
|
|
210
|
+
function lockTimeoutOf(input: TerraformApplyStepInput): { lockTimeout?: string } {
|
|
211
|
+
if (input.lockTimeout === false) return {};
|
|
212
|
+
const value = input.lockTimeout ?? DEFAULT_LOCK_TIMEOUT;
|
|
213
|
+
if (!isLockTimeout(value)) throw new Error(`terraform-apply: root "${input.root}": lockTimeout "${String(value)}" is not a duration such as "5m"`);
|
|
214
|
+
return { lockTimeout: value };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The step's input with its `reads` filled (#4545): each upstream's outputs
|
|
219
|
+
* read once, the values under the step's own `vars`. `waiting` when an
|
|
220
|
+
* upstream has no such output yet. Throws when a state cannot be read.
|
|
221
|
+
*/
|
|
222
|
+
async function withReads(input: TerraformApplyStepInput): Promise<{ input: TerraformApplyStepInput } | { waiting: string; upstreams: string[] }> {
|
|
223
|
+
const reads = input.reads ?? [];
|
|
224
|
+
if (reads.length === 0) return { input };
|
|
225
|
+
const problems = stateReadsProblems(reads);
|
|
226
|
+
if (problems.length > 0) throw new Error(`terraform-apply: root "${input.root}": ${problems.join("; ")}`);
|
|
227
|
+
const filled = await fillStateReads(reads, async (root) => {
|
|
228
|
+
const at = { root, ...(input.cwd ? { cwd: input.cwd } : {}) };
|
|
229
|
+
await terraformInit(at);
|
|
230
|
+
return terraformOutputs(at);
|
|
231
|
+
});
|
|
232
|
+
if (filled.errors.length > 0) throw new Error(`terraform-apply: root "${input.root}": ${filled.errors.join("; ")}`);
|
|
233
|
+
if (filled.waiting.length > 0) return { waiting: waitingReadsReason(filled.waiting), upstreams: [...new Set(filled.waiting.map((w) => w.read.root))].sort() };
|
|
234
|
+
return { input: { ...input, vars: { ...filled.vars, ...(input.vars ?? {}) } } };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** A root whose reads wait on an upstream (#4545): a hole naming it, and a digest over the root and those upstreams. */
|
|
238
|
+
function waitingOnReads(ctx: DeployContext, input: TerraformApplyStepInput, waiting: { waiting: string; upstreams: string[] }): CapabilityPlan {
|
|
239
|
+
const planDigest = computePlanDigest("terraform-apply-reads-waiting", { env: ctx.env, root: input.root, upstreams: waiting.upstreams });
|
|
240
|
+
const changeSet: ChangeSetPart = {
|
|
241
|
+
member: { member: input.root, lexicon: "terraform", planner: "chant", status: "planned", planDigest, holes: [{ address: input.root, type: "terraform_root", reason: waiting.waiting }] },
|
|
242
|
+
entries: [],
|
|
243
|
+
};
|
|
244
|
+
return { member: input.root, planDigest, changeSet };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const rootArgs = (input: TerraformApplyStepInput) => ({ root: input.root, ...(input.cwd ? { cwd: input.cwd } : {}), ...(input.dir ? { dir: input.dir } : {}) });
|
|
165
248
|
|
|
166
249
|
function requireRoot(input: TerraformApplyStepInput): void {
|
|
167
250
|
if (typeof input.root !== "string" || input.root === "") {
|
|
@@ -179,12 +262,27 @@ async function planRoot(input: TerraformApplyStepInput, targets?: string[]): Pro
|
|
|
179
262
|
...rootArgs(input),
|
|
180
263
|
...(input.vars ? { vars: input.vars } : {}),
|
|
181
264
|
...(targets && targets.length > 0 ? { targets } : {}),
|
|
265
|
+
...lockTimeoutOf(input),
|
|
266
|
+
...(input.cone && !(targets && targets.length > 0) ? { cone: input.cone } : {}),
|
|
182
267
|
text: false,
|
|
183
268
|
});
|
|
269
|
+
// #4545: the cone's targets join the digest, so an approval of this cone counts for no other.
|
|
270
|
+
const planDigest = conePlanDigest(planned.planDigest, planned.cone);
|
|
271
|
+
const changeSet = planned.changeSet && planDigest !== planned.planDigest ? { ...planned.changeSet, member: { ...planned.changeSet.member, planDigest } } : planned.changeSet;
|
|
272
|
+
// #4545: the plan document beside the plan file, which a wave's cost hold prices.
|
|
273
|
+
const planJson = join(planned.dir, `${planned.planFile}.json`);
|
|
274
|
+
let wrote = true;
|
|
275
|
+
try {
|
|
276
|
+
writeFileSync(planJson, JSON.stringify(planned.json));
|
|
277
|
+
} catch {
|
|
278
|
+
wrote = false;
|
|
279
|
+
}
|
|
184
280
|
return {
|
|
185
281
|
planFile: planned.planFile,
|
|
186
|
-
planDigest
|
|
187
|
-
...(
|
|
282
|
+
planDigest,
|
|
283
|
+
...(wrote ? { planJson } : {}),
|
|
284
|
+
...(planned.cone ? { cone: planned.cone } : {}),
|
|
285
|
+
...(changeSet ? { changeSet } : {}),
|
|
188
286
|
json: planned.json,
|
|
189
287
|
};
|
|
190
288
|
}
|
|
@@ -290,8 +388,9 @@ async function rootDir(input: TerraformApplyStepInput): Promise<{ dir: string; b
|
|
|
290
388
|
const start = resolve(input.cwd ?? process.cwd());
|
|
291
389
|
const { config, configPath } = await loadChantConfigUpward(start);
|
|
292
390
|
const terraform = (config as { terraform?: TerraformConfig }).terraform;
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
391
|
+
const declared = terraform?.roots?.[input.root]?.dir;
|
|
392
|
+
if (declared === undefined) return undefined;
|
|
393
|
+
const dir = input.dir ?? declared;
|
|
295
394
|
return { dir: resolve(configPath ? dirname(configPath) : start, dir), ...(terraform?.binary ? { binary: terraform.binary } : {}) };
|
|
296
395
|
}
|
|
297
396
|
|
|
@@ -364,11 +463,12 @@ async function previewRoot(ctx: DeployContext, input: TerraformApplyStepInput, u
|
|
|
364
463
|
}
|
|
365
464
|
|
|
366
465
|
/**
|
|
367
|
-
* choudoufu refused the plan `run` was handed by its wave (its exit 3)
|
|
368
|
-
* the live system moved since the plan
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
466
|
+
* choudoufu refused the plan `run` was handed by its wave (its exit 3)
|
|
467
|
+
* because the live system moved since the plan. Nothing was applied.
|
|
468
|
+
* `planMoved` is core's mark for a moved plan (`isPlanMovedError`), so the
|
|
469
|
+
* command that applied it exits `CHANT_EXIT.moved`, 4, as for the other
|
|
470
|
+
* capabilities that refuse a moved plan (#3997, #4553). A plan made against
|
|
471
|
+
* another estate is a plain failure (`terraformExitOf` in ../exit-codes).
|
|
372
472
|
*/
|
|
373
473
|
export class TerraformPlanMovedError extends Error {
|
|
374
474
|
readonly planMoved = true;
|
|
@@ -378,9 +478,6 @@ export class TerraformPlanMovedError extends Error {
|
|
|
378
478
|
}
|
|
379
479
|
}
|
|
380
480
|
|
|
381
|
-
/** `live-wave-apply`'s exit status for a set whose plan moved (`CHOUDOUFU_WAVE_EXIT.setMoved`). */
|
|
382
|
-
const CHOUDOUFU_SET_MOVED_EXIT = 3;
|
|
383
|
-
|
|
384
481
|
function isPlannedRoot(value: unknown): value is PlannedRoot {
|
|
385
482
|
const v = value as Partial<PlannedRoot> | undefined;
|
|
386
483
|
return typeof v?.planFile === "string" && typeof v.planDigest === "string";
|
|
@@ -390,6 +487,8 @@ function isPlannedRoot(value: unknown): value is PlannedRoot {
|
|
|
390
487
|
interface ChoudoufuWavePlannedRoot {
|
|
391
488
|
planDigest: string;
|
|
392
489
|
choudoufu: { planSetFile: string; setDigest: string };
|
|
490
|
+
/** The root's rows on the resource lock grain (#4552), from its embedded plan. */
|
|
491
|
+
lockRows?: LockRow[];
|
|
393
492
|
}
|
|
394
493
|
|
|
395
494
|
function isChoudoufuWavePlan(value: unknown): value is ChoudoufuWavePlannedRoot {
|
|
@@ -416,13 +515,28 @@ async function runChoudoufuWave(
|
|
|
416
515
|
if (applied.carry) ctx.carry?.(input.root, applied.carry);
|
|
417
516
|
if (!applied.landed) {
|
|
418
517
|
const message = `terraform-apply: root "${input.root}": ${applied.message}`;
|
|
419
|
-
if (applied.exitCode
|
|
518
|
+
if (terraformPlanMoved({ command: "live-wave-apply", code: applied.exitCode })) throw new TerraformPlanMovedError(input.root, message);
|
|
420
519
|
throw new Error(message);
|
|
421
520
|
}
|
|
422
521
|
const outputs = await terraformOutputs(rootArgs(input));
|
|
423
522
|
return { root: input.root, planDigest: planned.planDigest, fromWavePlan: true, outputs };
|
|
424
523
|
}
|
|
425
524
|
|
|
525
|
+
/** The input a `run` that plans for itself plans with (#4545): its reads filled, or an error naming the upstream to apply first. */
|
|
526
|
+
async function readsForRun(input: TerraformApplyStepInput): Promise<TerraformApplyStepInput> {
|
|
527
|
+
const read = await withReads(input);
|
|
528
|
+
if ("waiting" in read) throw new Error(`terraform-apply: root "${input.root}": ${read.waiting}`);
|
|
529
|
+
return read.input;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** The engine a root plans with (#4552): the configured binary's, else the plan's planner. */
|
|
533
|
+
async function plannerOf(input: TerraformApplyStepInput, plan?: CapabilityPlan): Promise<"terraform" | "tofu" | "choudoufu" | undefined> {
|
|
534
|
+
const binary = (await rootDir(input).catch(() => undefined))?.binary;
|
|
535
|
+
if (binary !== undefined) return plannerForBinary(binary) as "terraform" | "tofu" | "choudoufu";
|
|
536
|
+
const planner = plan?.changeSet?.member.planner;
|
|
537
|
+
return planner === "terraform" || planner === "tofu" || planner === "choudoufu" ? planner : "terraform";
|
|
538
|
+
}
|
|
539
|
+
|
|
426
540
|
export const terraformApplyCapability: Capability<TerraformApplyStepInput, TerraformApplyStepOutput> = {
|
|
427
541
|
kind: "terraform-apply",
|
|
428
542
|
// A root's apply has no undo short of applying the previous configuration,
|
|
@@ -430,26 +544,40 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
|
|
|
430
544
|
rollbackPolicy: "needs-opt-out",
|
|
431
545
|
// #4300: `--target` passes through as `-target`, and the saved plan holds those addresses alone.
|
|
432
546
|
targets: "accept",
|
|
433
|
-
|
|
434
|
-
|
|
547
|
+
// #4627: `plan` refreshes the root's state before it plans unless told `-refresh=false`, which chant never passes,
|
|
548
|
+
// so `--refresh` asks for what every plan does and the digest still binds it. `-target` plans a resource with what it
|
|
549
|
+
// depends on and never what depends on it, so `--target-dependents` has nothing to map onto.
|
|
550
|
+
planFlags: {
|
|
551
|
+
refresh: "accept",
|
|
552
|
+
targetDependents: { refuse: "terraform and tofu plan a -target with what it depends on, never what depends on it, so there is no flag to widen it to its dependents; name each dependent with --target" },
|
|
553
|
+
},
|
|
554
|
+
async plan(ctx: DeployContext, given: TerraformApplyStepInput): Promise<CapabilityPlan> {
|
|
555
|
+
requireRoot(given);
|
|
556
|
+
// #4545: a root reading an upstream's output that is not there yet is a hole naming the upstream.
|
|
557
|
+
const read = await withReads(given);
|
|
558
|
+
if ("waiting" in read) return waitingOnReads(ctx, given, read);
|
|
559
|
+
const { input } = read;
|
|
435
560
|
const unresolved = [...(ctx.unresolvedInputs ?? [])].sort();
|
|
436
561
|
const targets = ctx.targets && ctx.targets.length > 0 ? ctx.targets : undefined;
|
|
437
562
|
if (unresolved.length > 0) return previewRoot(ctx, input, unresolved, targets);
|
|
438
|
-
// A targeted root plans with `plan -target`, never as a choudoufu one-root set.
|
|
439
|
-
const wave = targets ? { wave: false } : await choudoufuWaveRoot({ ...rootArgs(input), ...(input.vars ? { vars: input.vars } : {}) });
|
|
563
|
+
// A targeted root plans with `plan -target`, and a cone plan with `plan -cone` (#4545), never as a choudoufu one-root set.
|
|
564
|
+
const wave = targets || input.cone ? { wave: false } : await choudoufuWaveRoot({ ...rootArgs(input), ...(input.vars ? { vars: input.vars } : {}) });
|
|
440
565
|
if (wave.wave) {
|
|
441
566
|
const set = await choudoufuPlanSet(rootArgs(input));
|
|
442
567
|
const artifact: ChoudoufuWavePlannedRoot = {
|
|
443
568
|
planDigest: set.planDigest,
|
|
444
569
|
choudoufu: { planSetFile: set.planSetFile, setDigest: set.setDigest },
|
|
570
|
+
lockRows: terraformLockRows(input.root, set.changeSet.member.scope, set.json),
|
|
445
571
|
};
|
|
446
572
|
return { member: input.root, planDigest: set.planDigest, artifact, changeSet: set.changeSet, outputs: terraformPlannedOutputs(set.json), shape: terraformPlanShape(set.json, input.root) };
|
|
447
573
|
}
|
|
448
574
|
const { changeSet, json, ...planned } = await planRoot(input, targets);
|
|
575
|
+
// #4552: a choudoufu root's rows, keyed from the plan document while it is at hand.
|
|
576
|
+
const lockRows = changeSet?.member.planner === "choudoufu" ? terraformLockRows(input.root, changeSet.member.scope, json) : undefined;
|
|
449
577
|
return {
|
|
450
578
|
member: input.root,
|
|
451
579
|
planDigest: planned.planDigest,
|
|
452
|
-
artifact: planned,
|
|
580
|
+
artifact: { ...planned, ...(lockRows ? { lockRows } : {}) },
|
|
453
581
|
...(changeSet ? { changeSet } : {}),
|
|
454
582
|
outputs: terraformPlannedOutputs(json),
|
|
455
583
|
shape: terraformPlanShape(json, input.root),
|
|
@@ -466,7 +594,29 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
|
|
|
466
594
|
// #4447 (ws-107): a group core forms of components that read each other plans and applies as one composed run
|
|
467
595
|
// when `terraform.compose.enabled` is set (./compose-group.ts). Delegates, so the methods are read when called.
|
|
468
596
|
groupReads: (ctx, members) => terraformComposeGroup.groupReads(ctx, members),
|
|
469
|
-
|
|
597
|
+
// #4542: a member pointed at a release's recorded copy plans alone, since a composition reads the declared roots.
|
|
598
|
+
groupFallback: async (ctx, members) =>
|
|
599
|
+
members.some((m) => typeof m.step.dir === "string")
|
|
600
|
+
? { kind: "release-copy", reason: "a member deploys a release's recorded copy of its root, which a composition does not read" }
|
|
601
|
+
: terraformComposeGroup.groupFallback(ctx, members),
|
|
602
|
+
// #4542: the root's files, which a release cut records and a promote under it plans.
|
|
603
|
+
releaseInputs: (input, ctx) => terraformReleaseInputs(input, ctx),
|
|
604
|
+
// #4552: resource under choudoufu, the root under Terraform, tofu and Terragrunt, which refuse resource by name.
|
|
605
|
+
async defaultLockGrain(input, plan) {
|
|
606
|
+
return terraformDefaultLockGrain(await plannerOf(input, plan));
|
|
607
|
+
},
|
|
608
|
+
async lockGrainRefusal(input, grain, plan) {
|
|
609
|
+
if (grain !== "resource") return undefined;
|
|
610
|
+
const binary = (await rootDir(input).catch(() => undefined))?.binary;
|
|
611
|
+
return terraformLockGrainRefusal(grain, await plannerOf(input, plan), binary);
|
|
612
|
+
},
|
|
613
|
+
lockRows(input, plan) {
|
|
614
|
+
const kept = (plan.artifact as { lockRows?: LockRow[] } | undefined)?.lockRows;
|
|
615
|
+
if (Array.isArray(kept)) return kept;
|
|
616
|
+
return plan.changeSet ? terraformLockRowsFromPart(input.root, plan.changeSet) : [];
|
|
617
|
+
},
|
|
618
|
+
// #4555: the root and the binary, for the step's span and run record.
|
|
619
|
+
traceFacts: async (input, outcome) => terraformTraceFacts(input, outcome, (await rootDir(input).catch(() => undefined))?.binary),
|
|
470
620
|
planGroup: (ctx, members) => terraformComposeGroup.planGroup(ctx, members),
|
|
471
621
|
runGroup: (ctx, run, members) => terraformComposeGroup.runGroup(ctx, run, members),
|
|
472
622
|
async run(ctx: DeployContext, input: TerraformApplyStepInput): Promise<TerraformApplyStepOutput> {
|
|
@@ -475,10 +625,10 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
|
|
|
475
625
|
if (isShapeArtifact(fromWave)) {
|
|
476
626
|
// A root previewed with stand-ins (#4463): plan it now with the real
|
|
477
627
|
// values, and apply that plan only when it is within the preview's shape.
|
|
478
|
-
const fresh = await planRoot(input, ctx.targets);
|
|
628
|
+
const fresh = await planRoot(await readsForRun(input), ctx.targets);
|
|
479
629
|
const differences = planShapeDifferences(fromWave.shape, terraformPlanShape(fresh.json, input.root));
|
|
480
630
|
if (differences.length > 0) throw new PlanShapeMovedError(input.root, differences);
|
|
481
|
-
const applied = await terraformApply({ ...rootArgs(input), planFile: fresh.planFile });
|
|
631
|
+
const applied = await terraformApply({ ...rootArgs(input), planFile: fresh.planFile, ...lockTimeoutOf(input) });
|
|
482
632
|
if (!applied.applied) {
|
|
483
633
|
throw new Error(`terraform-apply: root "${input.root}" refused the plan it was given (${applied.refused ?? "refused"}). ${applied.refusal ?? ""}`);
|
|
484
634
|
}
|
|
@@ -487,15 +637,16 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
|
|
|
487
637
|
}
|
|
488
638
|
const planned: PlannedRoot = isPlannedRoot(fromWave)
|
|
489
639
|
? fromWave
|
|
490
|
-
: await planRoot(input, ctx.targets).then(({ planFile, planDigest }) => ({ planFile, planDigest }));
|
|
491
|
-
const applied = await terraformApply({ ...rootArgs(input), planFile: planned.planFile });
|
|
640
|
+
: await planRoot(await readsForRun(input), ctx.targets).then(({ planFile, planDigest }) => ({ planFile, planDigest }));
|
|
641
|
+
const applied = await terraformApply({ ...rootArgs(input), planFile: planned.planFile, ...lockTimeoutOf(input) });
|
|
492
642
|
if (!applied.applied) {
|
|
493
643
|
const message =
|
|
494
644
|
`terraform-apply: root "${input.root}" refused the plan it was given (${applied.refused ?? "refused"}). ` +
|
|
495
645
|
(applied.refusal ?? "");
|
|
496
|
-
// A plan the wave approved that choudoufu refuses
|
|
497
|
-
//
|
|
498
|
-
|
|
646
|
+
// A plan the wave approved that choudoufu refuses as no longer matching
|
|
647
|
+
// moved since the approval (#4553: one made against another estate is a
|
|
648
|
+
// failure). One this run made itself is planned afresh by the next run.
|
|
649
|
+
if (applied.refused === "approval-mismatch" && isPlannedRoot(fromWave)) throw new TerraformPlanMovedError(input.root, message);
|
|
499
650
|
throw new Error(message);
|
|
500
651
|
}
|
|
501
652
|
const outputs = await terraformOutputs(rootArgs(input));
|
|
@@ -505,6 +656,9 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
|
|
|
505
656
|
|
|
506
657
|
export const TERRAFORM_VERB_FAMILIES = {
|
|
507
658
|
apply: ["terraform-apply"],
|
|
659
|
+
// #4618: the binary runs outside waves, each a fixed operation; the two that change state on their own run under a gated request.
|
|
660
|
+
state: ["terraform-state-unlock", "terraform-state-push"],
|
|
661
|
+
copy: ["terraform-copy"],
|
|
508
662
|
} as const;
|
|
509
663
|
|
|
510
664
|
export const terraformCapabilityPlugin: CapabilityPlugin = {
|
|
@@ -513,7 +667,7 @@ export const terraformCapabilityPlugin: CapabilityPlugin = {
|
|
|
513
667
|
return ownPackageVersion(import.meta.url);
|
|
514
668
|
},
|
|
515
669
|
capabilities(): Array<Capability<never, unknown>> {
|
|
516
|
-
return [terraformApplyCapability as Capability<never, unknown>];
|
|
670
|
+
return [terraformApplyCapability as Capability<never, unknown>, ...terraformStateOpCapabilities.map((c) => c as unknown as Capability<never, unknown>)];
|
|
517
671
|
},
|
|
518
672
|
families(): Record<string, readonly string[]> {
|
|
519
673
|
return TERRAFORM_VERB_FAMILIES;
|
|
@@ -341,7 +341,7 @@ function waitingGroup(ctx: GroupContext, members: readonly GroupMember<StepInput
|
|
|
341
341
|
return { members: plans, digest: computePlanDigest("terraform-apply-group-unresolved", { components: ctx.components, unresolved: waits }) };
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
/** A composed plan that no longer matches the one approved. Core's moved-plan mark, so nothing applies and the command exits
|
|
344
|
+
/** A composed plan that no longer matches the one approved. Core's moved-plan mark, so nothing applies and the command exits `CHANT_EXIT.moved`, 4 (#4553). */
|
|
345
345
|
export class ComposedPlanMovedError extends Error {
|
|
346
346
|
readonly planMoved = true;
|
|
347
347
|
constructor(message: string) {
|
|
@@ -287,7 +287,7 @@ chainSuite("with terraform.compose, three roots plan and apply as one group", {
|
|
|
287
287
|
writeFileSync(join(loop.dir, "roots/a/main.tf"), CHAIN.a!.replace("a-v1", "a-v4"));
|
|
288
288
|
loop.git("commit", "-qam", "a v4");
|
|
289
289
|
|
|
290
|
-
expect(await loop.merge(2)).toBe(
|
|
290
|
+
expect(await loop.merge(2)).toBe(4);
|
|
291
291
|
const refused = loop.report("apply");
|
|
292
292
|
expect(refused.status).toBe("refused");
|
|
293
293
|
expect(refused.refusal).toBe("plan-changed");
|
|
@@ -298,7 +298,7 @@ describe.skipIf(!BINARY)(`the pull-request loop over five ${BINARY ?? "terraform
|
|
|
298
298
|
write("roots/a/main.tf", ROOTS.a.replace("a-v1", "a-v4"));
|
|
299
299
|
git("commit", "-qam", "a v4");
|
|
300
300
|
|
|
301
|
-
expect(await merge(2)).toBe(
|
|
301
|
+
expect(await merge(2)).toBe(4);
|
|
302
302
|
const refused = report("apply");
|
|
303
303
|
expect(refused.status).toBe("refused");
|
|
304
304
|
expect(refused.refusal).toBe("plan-changed");
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files a release records for a `terraform-apply` step (chant#4542).
|
|
3
|
+
*
|
|
4
|
+
* The step names its root by key, so core can't see which files it deploys.
|
|
5
|
+
* `terraformReleaseInputs` answers core's `releaseInputs` with the set a
|
|
6
|
+
* pull request's plan treats as the root's change (`rootWatchPaths`): the
|
|
7
|
+
* root's directory, each local module it reaches, and its var files, plus
|
|
8
|
+
* its backend-config files. A release cut records them for the component,
|
|
9
|
+
* and a promote or rollback under the release writes them under
|
|
10
|
+
* `.chant/release-templates/<digest>/` and sets the step's `dir` to the copy
|
|
11
|
+
* of the root, so `terraform-apply` plans the release's configuration against
|
|
12
|
+
* the environment's state.
|
|
13
|
+
*
|
|
14
|
+
* A root whose state is a file in its own directory (the `local` backend) is
|
|
15
|
+
* unbound: a plan run in a copy elsewhere would read no state and propose
|
|
16
|
+
* creating everything. Such a root rolls back through `chant lifecycle
|
|
17
|
+
* rollback --to`. A live choudoufu root has no state file and is recorded.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { dirname, relative, resolve } from "node:path";
|
|
21
|
+
import type { ReleaseInputs, ReleaseInputsContext, ReleaseInputsUnbound } from "@intentius/chant/components/capability";
|
|
22
|
+
import { loadChantConfigUpward } from "@intentius/chant/config";
|
|
23
|
+
import { rootWatchPaths } from "../changed-roots";
|
|
24
|
+
import type { TerraformConfig } from "../config";
|
|
25
|
+
import { liveEstate } from "../compose/hcl";
|
|
26
|
+
import { readBackend } from "../state/backend";
|
|
27
|
+
|
|
28
|
+
/** The parts of the `terraform-apply` input this reads. */
|
|
29
|
+
interface StepInput {
|
|
30
|
+
root: string;
|
|
31
|
+
cwd?: string;
|
|
32
|
+
dir?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const toPosix = (p: string): string => p.split("\\").join("/") || ".";
|
|
36
|
+
|
|
37
|
+
/** What a release records for one `terraform-apply` step, relative to the member's directory. Null when the root is not declared. */
|
|
38
|
+
export async function terraformReleaseInputs(input: StepInput, ctx: ReleaseInputsContext): Promise<ReleaseInputs | ReleaseInputsUnbound | null> {
|
|
39
|
+
if (typeof input.root !== "string" || input.root === "") return null;
|
|
40
|
+
const start = resolve(ctx.memberDir, input.cwd ?? ".");
|
|
41
|
+
const { config, configPath } = await loadChantConfigUpward(start);
|
|
42
|
+
const projectRoot = configPath ? dirname(configPath) : start;
|
|
43
|
+
const terraform = (config as { terraform?: TerraformConfig }).terraform;
|
|
44
|
+
const root = terraform?.roots?.[input.root];
|
|
45
|
+
if (!root) return null;
|
|
46
|
+
const binary = terraform?.binary;
|
|
47
|
+
const rootDir = resolve(projectRoot, input.dir ?? root.dir);
|
|
48
|
+
const live = binary === "choudoufu" && (root.estate !== undefined || liveEstate(rootDir, binary).live);
|
|
49
|
+
if (!live) {
|
|
50
|
+
const { backend } = readBackend(rootDir, {
|
|
51
|
+
...(root.backendConfig ? { backendConfig: root.backendConfig } : {}),
|
|
52
|
+
...(root.backendConfigFiles ? { backendConfigFiles: root.backendConfigFiles } : {}),
|
|
53
|
+
...(binary ? { binary } : {}),
|
|
54
|
+
});
|
|
55
|
+
if (backend.type === "local") {
|
|
56
|
+
return {
|
|
57
|
+
unbound:
|
|
58
|
+
`root "${input.root}" keeps its state in a local file under ${toPosix(relative(ctx.memberDir, rootDir))}, ` +
|
|
59
|
+
"so a plan of a recorded copy would read no state; roll it back with chant lifecycle rollback --to <ref>",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const fromMember = (abs: string): string => toPosix(relative(ctx.memberDir, abs));
|
|
64
|
+
const paths = rootWatchPaths(projectRoot, { dir: relative(projectRoot, rootDir) || ".", ...(root.varFiles ? { varFiles: root.varFiles } : {}) }, binary).map((p) =>
|
|
65
|
+
fromMember(resolve(projectRoot, p)),
|
|
66
|
+
);
|
|
67
|
+
for (const file of root.backendConfigFiles ?? []) paths.push(fromMember(resolve(rootDir, file)));
|
|
68
|
+
return { field: "dir", dir: fromMember(rootDir), paths: [...new Set(paths)].sort() };
|
|
69
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `terraform-apply`'s engine facts (INTENTIUS/chant#4555): the root and the
|
|
3
|
+
* binary, for the step's span and run record.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import { terraformTraceFacts } from "./trace-facts";
|
|
8
|
+
|
|
9
|
+
describe("terraform-apply's trace facts (#4555)", () => {
|
|
10
|
+
test("name the root, the configured binary and whether the wave's plan applied", () => {
|
|
11
|
+
expect(terraformTraceFacts({ root: "network" }, { output: { root: "network", planDigest: "sha256:x", fromWavePlan: true, outputs: { vpc_id: "v" } } }, "/usr/local/bin/tofu")).toEqual({
|
|
12
|
+
attributes: { root: "network", binary: "tofu", "apply.from_wave_plan": true, "apply.outputs": 1 },
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("default to terraform, and keep the root and the cone on a failed apply", () => {
|
|
17
|
+
expect(terraformTraceFacts({ root: "network", cone: "HEAD~1" }, { error: new Error("refused") }, undefined)).toEqual({
|
|
18
|
+
attributes: { root: "network", binary: "terraform", "plan.cone": "HEAD~1" },
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `terraform-apply`'s engine facts for its span and op-wave run record
|
|
3
|
+
* (INTENTIUS/chant#4555): the root, the binary that applied it, and whether
|
|
4
|
+
* it applied the plan the wave decided. Core adds the plan's resource counts
|
|
5
|
+
* by action, and writes every key under the CI product's prefix
|
|
6
|
+
* (`terragucci.root`, `terragucci.binary`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CapabilityTraceFacts } from "@intentius/chant/otlp";
|
|
10
|
+
import type { TerraformApplyStepInput, TerraformApplyStepOutput } from "./capability-plugin";
|
|
11
|
+
|
|
12
|
+
/** `traceFacts` for `terraform-apply` (#4555). `binary` is the config's `terraform.binary`, `terraform` when it names none. */
|
|
13
|
+
export function terraformTraceFacts(
|
|
14
|
+
input: TerraformApplyStepInput,
|
|
15
|
+
outcome: { output: TerraformApplyStepOutput } | { error: unknown },
|
|
16
|
+
binary: string | undefined,
|
|
17
|
+
): CapabilityTraceFacts {
|
|
18
|
+
const bin = (binary ?? "terraform").split(/[\\/]/).pop() || "terraform";
|
|
19
|
+
return {
|
|
20
|
+
attributes: {
|
|
21
|
+
root: input.root,
|
|
22
|
+
binary: bin,
|
|
23
|
+
...("output" in outcome && outcome.output ? { "apply.from_wave_plan": outcome.output.fromWavePlan, "apply.outputs": Object.keys(outcome.output.outputs ?? {}).length } : {}),
|
|
24
|
+
...(input.cone ? { "plan.cone": input.cone } : {}),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|