@intentius/chant-lexicon-terraform 0.124.54 → 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 +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 +167 -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 +178 -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[];
|
|
207
|
+
}
|
|
208
|
+
|
|
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 };
|
|
162
245
|
}
|
|
163
246
|
|
|
164
|
-
const rootArgs = (input: TerraformApplyStepInput) => ({ root: input.root, ...(input.cwd ? { cwd: input.cwd } : {}) });
|
|
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,33 @@ 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
|
-
async plan(ctx: DeployContext,
|
|
434
|
-
requireRoot(
|
|
547
|
+
async plan(ctx: DeployContext, given: TerraformApplyStepInput): Promise<CapabilityPlan> {
|
|
548
|
+
requireRoot(given);
|
|
549
|
+
// #4545: a root reading an upstream's output that is not there yet is a hole naming the upstream.
|
|
550
|
+
const read = await withReads(given);
|
|
551
|
+
if ("waiting" in read) return waitingOnReads(ctx, given, read);
|
|
552
|
+
const { input } = read;
|
|
435
553
|
const unresolved = [...(ctx.unresolvedInputs ?? [])].sort();
|
|
436
554
|
const targets = ctx.targets && ctx.targets.length > 0 ? ctx.targets : undefined;
|
|
437
555
|
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 } : {}) });
|
|
556
|
+
// A targeted root plans with `plan -target`, and a cone plan with `plan -cone` (#4545), never as a choudoufu one-root set.
|
|
557
|
+
const wave = targets || input.cone ? { wave: false } : await choudoufuWaveRoot({ ...rootArgs(input), ...(input.vars ? { vars: input.vars } : {}) });
|
|
440
558
|
if (wave.wave) {
|
|
441
559
|
const set = await choudoufuPlanSet(rootArgs(input));
|
|
442
560
|
const artifact: ChoudoufuWavePlannedRoot = {
|
|
443
561
|
planDigest: set.planDigest,
|
|
444
562
|
choudoufu: { planSetFile: set.planSetFile, setDigest: set.setDigest },
|
|
563
|
+
lockRows: terraformLockRows(input.root, set.changeSet.member.scope, set.json),
|
|
445
564
|
};
|
|
446
565
|
return { member: input.root, planDigest: set.planDigest, artifact, changeSet: set.changeSet, outputs: terraformPlannedOutputs(set.json), shape: terraformPlanShape(set.json, input.root) };
|
|
447
566
|
}
|
|
448
567
|
const { changeSet, json, ...planned } = await planRoot(input, targets);
|
|
568
|
+
// #4552: a choudoufu root's rows, keyed from the plan document while it is at hand.
|
|
569
|
+
const lockRows = changeSet?.member.planner === "choudoufu" ? terraformLockRows(input.root, changeSet.member.scope, json) : undefined;
|
|
449
570
|
return {
|
|
450
571
|
member: input.root,
|
|
451
572
|
planDigest: planned.planDigest,
|
|
452
|
-
artifact: planned,
|
|
573
|
+
artifact: { ...planned, ...(lockRows ? { lockRows } : {}) },
|
|
453
574
|
...(changeSet ? { changeSet } : {}),
|
|
454
575
|
outputs: terraformPlannedOutputs(json),
|
|
455
576
|
shape: terraformPlanShape(json, input.root),
|
|
@@ -466,7 +587,29 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
|
|
|
466
587
|
// #4447 (ws-107): a group core forms of components that read each other plans and applies as one composed run
|
|
467
588
|
// when `terraform.compose.enabled` is set (./compose-group.ts). Delegates, so the methods are read when called.
|
|
468
589
|
groupReads: (ctx, members) => terraformComposeGroup.groupReads(ctx, members),
|
|
469
|
-
|
|
590
|
+
// #4542: a member pointed at a release's recorded copy plans alone, since a composition reads the declared roots.
|
|
591
|
+
groupFallback: async (ctx, members) =>
|
|
592
|
+
members.some((m) => typeof m.step.dir === "string")
|
|
593
|
+
? { kind: "release-copy", reason: "a member deploys a release's recorded copy of its root, which a composition does not read" }
|
|
594
|
+
: terraformComposeGroup.groupFallback(ctx, members),
|
|
595
|
+
// #4542: the root's files, which a release cut records and a promote under it plans.
|
|
596
|
+
releaseInputs: (input, ctx) => terraformReleaseInputs(input, ctx),
|
|
597
|
+
// #4552: resource under choudoufu, the root under Terraform, tofu and Terragrunt, which refuse resource by name.
|
|
598
|
+
async defaultLockGrain(input, plan) {
|
|
599
|
+
return terraformDefaultLockGrain(await plannerOf(input, plan));
|
|
600
|
+
},
|
|
601
|
+
async lockGrainRefusal(input, grain, plan) {
|
|
602
|
+
if (grain !== "resource") return undefined;
|
|
603
|
+
const binary = (await rootDir(input).catch(() => undefined))?.binary;
|
|
604
|
+
return terraformLockGrainRefusal(grain, await plannerOf(input, plan), binary);
|
|
605
|
+
},
|
|
606
|
+
lockRows(input, plan) {
|
|
607
|
+
const kept = (plan.artifact as { lockRows?: LockRow[] } | undefined)?.lockRows;
|
|
608
|
+
if (Array.isArray(kept)) return kept;
|
|
609
|
+
return plan.changeSet ? terraformLockRowsFromPart(input.root, plan.changeSet) : [];
|
|
610
|
+
},
|
|
611
|
+
// #4555: the root and the binary, for the step's span and run record.
|
|
612
|
+
traceFacts: async (input, outcome) => terraformTraceFacts(input, outcome, (await rootDir(input).catch(() => undefined))?.binary),
|
|
470
613
|
planGroup: (ctx, members) => terraformComposeGroup.planGroup(ctx, members),
|
|
471
614
|
runGroup: (ctx, run, members) => terraformComposeGroup.runGroup(ctx, run, members),
|
|
472
615
|
async run(ctx: DeployContext, input: TerraformApplyStepInput): Promise<TerraformApplyStepOutput> {
|
|
@@ -475,10 +618,10 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
|
|
|
475
618
|
if (isShapeArtifact(fromWave)) {
|
|
476
619
|
// A root previewed with stand-ins (#4463): plan it now with the real
|
|
477
620
|
// values, and apply that plan only when it is within the preview's shape.
|
|
478
|
-
const fresh = await planRoot(input, ctx.targets);
|
|
621
|
+
const fresh = await planRoot(await readsForRun(input), ctx.targets);
|
|
479
622
|
const differences = planShapeDifferences(fromWave.shape, terraformPlanShape(fresh.json, input.root));
|
|
480
623
|
if (differences.length > 0) throw new PlanShapeMovedError(input.root, differences);
|
|
481
|
-
const applied = await terraformApply({ ...rootArgs(input), planFile: fresh.planFile });
|
|
624
|
+
const applied = await terraformApply({ ...rootArgs(input), planFile: fresh.planFile, ...lockTimeoutOf(input) });
|
|
482
625
|
if (!applied.applied) {
|
|
483
626
|
throw new Error(`terraform-apply: root "${input.root}" refused the plan it was given (${applied.refused ?? "refused"}). ${applied.refusal ?? ""}`);
|
|
484
627
|
}
|
|
@@ -487,15 +630,16 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
|
|
|
487
630
|
}
|
|
488
631
|
const planned: PlannedRoot = isPlannedRoot(fromWave)
|
|
489
632
|
? fromWave
|
|
490
|
-
: await planRoot(input, ctx.targets).then(({ planFile, planDigest }) => ({ planFile, planDigest }));
|
|
491
|
-
const applied = await terraformApply({ ...rootArgs(input), planFile: planned.planFile });
|
|
633
|
+
: await planRoot(await readsForRun(input), ctx.targets).then(({ planFile, planDigest }) => ({ planFile, planDigest }));
|
|
634
|
+
const applied = await terraformApply({ ...rootArgs(input), planFile: planned.planFile, ...lockTimeoutOf(input) });
|
|
492
635
|
if (!applied.applied) {
|
|
493
636
|
const message =
|
|
494
637
|
`terraform-apply: root "${input.root}" refused the plan it was given (${applied.refused ?? "refused"}). ` +
|
|
495
638
|
(applied.refusal ?? "");
|
|
496
|
-
// A plan the wave approved that choudoufu refuses
|
|
497
|
-
//
|
|
498
|
-
|
|
639
|
+
// A plan the wave approved that choudoufu refuses as no longer matching
|
|
640
|
+
// moved since the approval (#4553: one made against another estate is a
|
|
641
|
+
// failure). One this run made itself is planned afresh by the next run.
|
|
642
|
+
if (applied.refused === "approval-mismatch" && isPlannedRoot(fromWave)) throw new TerraformPlanMovedError(input.root, message);
|
|
499
643
|
throw new Error(message);
|
|
500
644
|
}
|
|
501
645
|
const outputs = await terraformOutputs(rootArgs(input));
|
|
@@ -505,6 +649,9 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
|
|
|
505
649
|
|
|
506
650
|
export const TERRAFORM_VERB_FAMILIES = {
|
|
507
651
|
apply: ["terraform-apply"],
|
|
652
|
+
// #4618: the binary runs outside waves, each a fixed operation; the two that change state on their own run under a gated request.
|
|
653
|
+
state: ["terraform-state-unlock", "terraform-state-push"],
|
|
654
|
+
copy: ["terraform-copy"],
|
|
508
655
|
} as const;
|
|
509
656
|
|
|
510
657
|
export const terraformCapabilityPlugin: CapabilityPlugin = {
|
|
@@ -513,7 +660,7 @@ export const terraformCapabilityPlugin: CapabilityPlugin = {
|
|
|
513
660
|
return ownPackageVersion(import.meta.url);
|
|
514
661
|
},
|
|
515
662
|
capabilities(): Array<Capability<never, unknown>> {
|
|
516
|
-
return [terraformApplyCapability as Capability<never, unknown>];
|
|
663
|
+
return [terraformApplyCapability as Capability<never, unknown>, ...terraformStateOpCapabilities.map((c) => c as unknown as Capability<never, unknown>)];
|
|
517
664
|
},
|
|
518
665
|
families(): Record<string, readonly string[]> {
|
|
519
666
|
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
|
+
}
|
package/src/cone.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* choudoufu's cone plans (#4545, moved from terragucci's `cone.ts`): `plan
|
|
3
|
+
* -cone=<base>` plans only the blocks a change touches and every block that
|
|
4
|
+
* reads them.
|
|
5
|
+
*
|
|
6
|
+
* A wave that applies an open pull request plans a live root as the cone of
|
|
7
|
+
* its change since the pull request's base, as the plan note did. A wave
|
|
8
|
+
* that applies a merge commit and a drift run plan the whole estate, as do
|
|
9
|
+
* a root with no `live` block, an estate file (`estate.chdf.hcl`), whose
|
|
10
|
+
* form choudoufu refuses `-cone` on, and any binary but choudoufu.
|
|
11
|
+
*
|
|
12
|
+
* choudoufu's guard can plan the whole estate anyway (no base, a provider,
|
|
13
|
+
* terraform or live block or the lock file changed, a cone over 30% of the
|
|
14
|
+
* estate's blocks, and so on). It says why in a warning. A cone plan says
|
|
15
|
+
* its size in a warning too, and with `-out FILE` writes `FILE.cone.json`:
|
|
16
|
+
* the cone's base, the commit it named and the addresses it targets.
|
|
17
|
+
*
|
|
18
|
+
* The record's targets join the root's plan digest ({@link conePlanDigest}),
|
|
19
|
+
* so the digest an approval binds names the cone. A wave that plans again
|
|
20
|
+
* after its approval and finds another cone has another digest, and refuses
|
|
21
|
+
* as moved. choudoufu's `apply FILE` checks the same record again.
|
|
22
|
+
*
|
|
23
|
+
* Pure, apart from reading the record beside a plan file.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
26
|
+
import { basename } from "node:path";
|
|
27
|
+
import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
|
|
28
|
+
|
|
29
|
+
/** The flags choudoufu refuses beside `-cone`, or plans in full with: a plan that carries one gets no `-cone`. */
|
|
30
|
+
export const CONE_EXCLUDES = ["-target", "-exclude", "-destroy", "-refresh-only", "-json", "-estate"] as const;
|
|
31
|
+
|
|
32
|
+
/** What choudoufu writes beside a saved cone plan, `FILE.cone.json`. */
|
|
33
|
+
export interface ConeRecord {
|
|
34
|
+
base: string;
|
|
35
|
+
base_sha: string;
|
|
36
|
+
targets: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** What one root's cone plan did: planned the cone, or planned the whole estate for the guard's reason. */
|
|
40
|
+
export interface PlanCone {
|
|
41
|
+
/** The revision the plan was asked to measure the change from. */
|
|
42
|
+
base: string;
|
|
43
|
+
/** With a cone: its blocks, and the estate's, as choudoufu counted them. Absent when it did not say. */
|
|
44
|
+
blocks?: number;
|
|
45
|
+
of?: number;
|
|
46
|
+
/** With a cone: the record beside the plan file. */
|
|
47
|
+
record?: ConeRecord;
|
|
48
|
+
/** The guard planned the whole estate: its kind and why. */
|
|
49
|
+
fallback?: { kind: string; reason: string };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether `binary` (a name or a path) is choudoufu. */
|
|
53
|
+
export const isChoudoufu = (binary: string): boolean => basename(binary) === "choudoufu";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The `-cone` argument a plan takes: `-cone=<base>` for choudoufu with a
|
|
57
|
+
* base, on a root that takes it (`live`), when none of `args` is a flag in
|
|
58
|
+
* {@link CONE_EXCLUDES}. Otherwise none, and the plan is full.
|
|
59
|
+
*/
|
|
60
|
+
export function coneArgs(binary: string, base: string | undefined, args: readonly string[], cone: boolean): string[] {
|
|
61
|
+
if (!base || !cone || !isChoudoufu(binary)) return [];
|
|
62
|
+
const excluded = args.some((a) => CONE_EXCLUDES.some((f) => a === f || a.startsWith(`${f}=`)));
|
|
63
|
+
return excluded ? [] : [`-cone=${base}`];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The path of the cone record beside a plan file. */
|
|
67
|
+
export const coneRecordPath = (planFile: string): string => `${planFile}.cone.json`;
|
|
68
|
+
|
|
69
|
+
/** The cone record beside `planFile`, or none when there is none or it does not read as one. */
|
|
70
|
+
export function readConeRecord(planFile: string): ConeRecord | undefined {
|
|
71
|
+
const path = coneRecordPath(planFile);
|
|
72
|
+
if (!existsSync(path)) return undefined;
|
|
73
|
+
try {
|
|
74
|
+
const r = JSON.parse(readFileSync(path, "utf-8")) as Partial<ConeRecord> | null;
|
|
75
|
+
if (!r || typeof r.base !== "string" || !Array.isArray(r.targets) || !r.targets.every((t) => typeof t === "string")) return undefined;
|
|
76
|
+
return { base: r.base, base_sha: typeof r.base_sha === "string" ? r.base_sha : "", targets: [...r.targets].sort() };
|
|
77
|
+
} catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The detail of the last warning with this summary in a plan's output, its
|
|
84
|
+
* lines joined. The binary prints a warning as `Warning: <summary>`, a blank
|
|
85
|
+
* line, and the detail up to the next blank line or the plan's summary; a box
|
|
86
|
+
* drawn around it with colour on is stripped.
|
|
87
|
+
*/
|
|
88
|
+
function warningDetail(text: string, summary: string): string | undefined {
|
|
89
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n").map((l) => l.replace(/^[\s│╷╵]*/, "").trimEnd());
|
|
90
|
+
let found: string | undefined;
|
|
91
|
+
for (let i = 0; i < lines.length; i++) {
|
|
92
|
+
if (lines[i] !== `Warning: ${summary}`) continue;
|
|
93
|
+
let j = i + 1;
|
|
94
|
+
while (j < lines.length && lines[j] === "") j++;
|
|
95
|
+
const detail: string[] = [];
|
|
96
|
+
for (; j < lines.length && lines[j] !== "" && !/^(Plan: |No changes\.|Changes to Outputs:)/.test(lines[j]!); j++) detail.push(lines[j]!);
|
|
97
|
+
found = detail.join(" ").replace(/\s+/g, " ").trim();
|
|
98
|
+
}
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* What a plan run with `-cone=<base>` did, from its output and the record
|
|
104
|
+
* beside `planFile`. A fallback warning means the whole estate was planned,
|
|
105
|
+
* even after a cone warning. With no fallback, the record is the cone. With
|
|
106
|
+
* neither, choudoufu said nothing this reads, and the plan counts as full.
|
|
107
|
+
*/
|
|
108
|
+
export function readPlanCone(output: string, planFile: string, base: string): PlanCone {
|
|
109
|
+
const fell = warningDetail(output, "Cone plan fell back to a full plan");
|
|
110
|
+
if (fell !== undefined) {
|
|
111
|
+
const m = /^This plan covers the whole estate \(guard ([^)]*)\): (.*?)\.?$/.exec(fell);
|
|
112
|
+
return { base, fallback: m ? { kind: m[1]!, reason: m[2]! } : { kind: "unknown", reason: fell } };
|
|
113
|
+
}
|
|
114
|
+
const record = readConeRecord(planFile);
|
|
115
|
+
if (!record) return { base, fallback: { kind: "unknown", reason: "choudoufu wrote no cone record beside the plan" } };
|
|
116
|
+
const size = /: (\d+) of the estate's (\d+) blocks/.exec(warningDetail(output, "Cone plan") ?? "");
|
|
117
|
+
return { base, ...(size ? { blocks: Number(size[1]), of: Number(size[2]) } : {}), record };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** What a cone plan did, in words: its size, or why it planned the whole estate. */
|
|
121
|
+
export function coneText(cone: PlanCone): string {
|
|
122
|
+
if (cone.fallback) return `the whole estate, since ${cone.fallback.reason} (guard ${cone.fallback.kind})`;
|
|
123
|
+
const n = cone.record?.targets.length ?? 0;
|
|
124
|
+
const size = cone.blocks !== undefined && cone.of !== undefined ? `${cone.blocks} of the estate's ${cone.of} blocks` : `${n} target${n === 1 ? "" : "s"}`;
|
|
125
|
+
return `the cone of the change since ${cone.base}, ${size}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A root's plan digest with its cone in it: the plan's own digest, unchanged,
|
|
130
|
+
* when the plan was full; with a cone, the digest over the plan's digest and
|
|
131
|
+
* the cone's targets, so an approval of one cone counts for no other. The
|
|
132
|
+
* digest's kind is terragucci's, so approvals recorded before the move still
|
|
133
|
+
* name the same digest.
|
|
134
|
+
*/
|
|
135
|
+
export function conePlanDigest(planDigest: string, cone: PlanCone | undefined): string {
|
|
136
|
+
if (!cone?.record) return planDigest;
|
|
137
|
+
return computePlanDigest("terragucci-cone-plan", { plan: planDigest, targets: [...cone.record.targets].sort() });
|
|
138
|
+
}
|