@intentius/chant-lexicon-terraform 0.124.51 → 0.124.54
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 -6
- package/dist/__fixtures__/pr-loop/estate.component.d.ts +7 -0
- package/dist/__fixtures__/pr-loop/estate.component.d.ts.map +1 -0
- package/dist/__fixtures__/pr-loop/estate.component.js +7 -0
- package/dist/__fixtures__/report/generate.d.ts +26 -0
- package/dist/__fixtures__/report/generate.d.ts.map +1 -0
- package/dist/__fixtures__/report/generate.js +58 -0
- package/dist/applied-state.d.ts +27 -0
- package/dist/applied-state.d.ts.map +1 -0
- package/dist/applied-state.js +44 -0
- package/dist/backend-config.d.ts +53 -0
- package/dist/backend-config.d.ts.map +1 -0
- package/dist/backend-config.js +136 -0
- package/dist/behaviour/kinds.d.ts.map +1 -1
- package/dist/behaviour/kinds.js +4 -0
- package/dist/behaviour/predict.d.ts.map +1 -1
- package/dist/behaviour/predict.js +6 -2
- package/dist/behaviour/reference-catalog.d.ts +16 -5
- package/dist/behaviour/reference-catalog.d.ts.map +1 -1
- package/dist/behaviour/reference-catalog.js +16 -5
- package/dist/behaviour/request.d.ts.map +1 -1
- package/dist/behaviour/request.js +3 -2
- package/dist/changed-roots.d.ts +6 -4
- package/dist/changed-roots.d.ts.map +1 -1
- package/dist/changed-roots.js +14 -18
- package/dist/commands.d.ts +5 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +87 -4
- package/dist/components/capability-plugin.d.ts +65 -3
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +197 -13
- package/dist/components/compose-group.d.ts +145 -0
- package/dist/components/compose-group.d.ts.map +1 -0
- package/dist/components/compose-group.js +407 -0
- package/dist/components/placeholder-preview.d.ts +78 -0
- package/dist/components/placeholder-preview.d.ts.map +1 -0
- package/dist/components/placeholder-preview.js +254 -0
- package/dist/compose/apply-events.d.ts +46 -0
- package/dist/compose/apply-events.d.ts.map +1 -0
- package/dist/compose/apply-events.js +77 -0
- package/dist/compose/estate.d.ts +194 -0
- package/dist/compose/estate.d.ts.map +1 -0
- package/dist/compose/estate.js +363 -0
- package/dist/compose/hcl.d.ts +99 -0
- package/dist/compose/hcl.d.ts.map +1 -0
- package/dist/compose/hcl.js +302 -0
- package/dist/compose/module-address.d.ts +24 -0
- package/dist/compose/module-address.d.ts.map +1 -0
- package/dist/compose/module-address.js +43 -0
- package/dist/compose/root.d.ts +127 -0
- package/dist/compose/root.d.ts.map +1 -0
- package/dist/compose/root.js +586 -0
- package/dist/compose/run.d.ts +183 -0
- package/dist/compose/run.d.ts.map +1 -0
- package/dist/compose/run.js +447 -0
- package/dist/compose/slice.d.ts +102 -0
- package/dist/compose/slice.d.ts.map +1 -0
- package/dist/compose/slice.js +249 -0
- package/dist/compose/state-address.d.ts +28 -0
- package/dist/compose/state-address.d.ts.map +1 -0
- package/dist/compose/state-address.js +154 -0
- package/dist/compose/state-merge.d.ts +52 -0
- package/dist/compose/state-merge.d.ts.map +1 -0
- package/dist/compose/state-merge.js +135 -0
- package/dist/compose.d.ts +179 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +482 -0
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-apply-op.js +7 -3
- package/dist/config.d.ts +59 -13
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -9
- package/dist/describe-resources.d.ts +74 -12
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/describe-resources.js +374 -66
- package/dist/drift-read.d.ts +33 -0
- package/dist/drift-read.d.ts.map +1 -0
- package/dist/drift-read.js +243 -0
- package/dist/drift-record.d.ts +147 -0
- package/dist/drift-record.d.ts.map +1 -0
- package/dist/drift-record.js +383 -0
- package/dist/hcl/edges.d.ts.map +1 -1
- package/dist/hcl/edges.js +24 -1
- package/dist/hcl/files.d.ts +61 -0
- package/dist/hcl/files.d.ts.map +1 -0
- package/dist/hcl/files.js +221 -0
- package/dist/hcl/instances.d.ts +82 -0
- package/dist/hcl/instances.d.ts.map +1 -0
- package/dist/hcl/instances.js +390 -0
- package/dist/hcl/parse.d.ts +81 -9
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/parse.js +114 -16
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/roots.js +5 -0
- package/dist/hcl/suppressions.d.ts.map +1 -1
- package/dist/hcl/suppressions.js +8 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -2
- package/dist/integrity.json +3 -3
- package/dist/live-plan-reasons.d.ts +21 -0
- package/dist/live-plan-reasons.d.ts.map +1 -0
- package/dist/live-plan-reasons.js +47 -0
- package/dist/lsp/option-keys.d.ts.map +1 -1
- package/dist/lsp/option-keys.js +2 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.d.ts +6 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +5 -1
- package/dist/op/activities/live-detect.d.ts +4 -3
- package/dist/op/activities/live-detect.d.ts.map +1 -1
- package/dist/op/activities/live-detect.js +57 -32
- package/dist/op/activities/pin-rollout.d.ts +2 -0
- package/dist/op/activities/pin-rollout.d.ts.map +1 -1
- package/dist/op/activities/pin-rollout.js +1 -0
- package/dist/op/activities/terraform.d.ts +153 -5
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activities/terraform.js +205 -20
- package/dist/op/activity-contracts.d.ts +27 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/op/activity-contracts.js +19 -1
- package/dist/op/builders.d.ts +7 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +9 -0
- package/dist/pin/edit.d.ts.map +1 -1
- package/dist/pin/edit.js +35 -0
- package/dist/pin/rollout.d.ts +2 -0
- package/dist/pin/rollout.d.ts.map +1 -1
- package/dist/pin/rollout.js +14 -11
- package/dist/plan-shape.d.ts +30 -0
- package/dist/plan-shape.d.ts.map +1 -0
- package/dist/plan-shape.js +125 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +32 -18
- package/dist/report.d.ts +216 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +666 -0
- package/dist/skills/chant-terraform.md +1 -1
- package/dist/state/azure-blob.d.ts +68 -0
- package/dist/state/azure-blob.d.ts.map +1 -0
- package/dist/state/azure-blob.js +201 -0
- package/dist/state/backend.d.ts +158 -0
- package/dist/state/backend.d.ts.map +1 -0
- package/dist/state/backend.js +406 -0
- package/dist/state/exec.d.ts +39 -0
- package/dist/state/exec.d.ts.map +1 -0
- package/dist/state/exec.js +80 -0
- package/dist/state/gcs.d.ts +62 -0
- package/dist/state/gcs.d.ts.map +1 -0
- package/dist/state/gcs.js +224 -0
- package/dist/state/index.d.ts +15 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +14 -0
- package/dist/state/locks.d.ts +105 -0
- package/dist/state/locks.d.ts.map +1 -0
- package/dist/state/locks.js +130 -0
- package/dist/state/object-store.d.ts +37 -0
- package/dist/state/object-store.d.ts.map +1 -0
- package/dist/state/object-store.js +20 -0
- package/dist/state/root-states.d.ts +131 -0
- package/dist/state/root-states.d.ts.map +1 -0
- package/dist/state/root-states.js +241 -0
- package/dist/state/s3.d.ts +68 -0
- package/dist/state/s3.d.ts.map +1 -0
- package/dist/state/s3.js +170 -0
- package/dist/state/store.d.ts +51 -0
- package/dist/state/store.d.ts.map +1 -0
- package/dist/state/store.js +185 -0
- package/dist/terragrunt/affected.d.ts.map +1 -1
- package/dist/terragrunt/affected.js +2 -1
- package/package.json +7 -3
- package/src/__fixtures__/ACCEPTANCE.md +69 -7
- package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
- package/src/__fixtures__/compose/state-app.json +66 -0
- package/src/__fixtures__/compose/state-edge.json +40 -0
- package/src/__fixtures__/compose/state-net.json +58 -0
- package/src/__fixtures__/compose/three-app.plan.json +184 -0
- package/src/__fixtures__/compose/three-composed.plan.json +509 -0
- package/src/__fixtures__/compose/three-edge.plan.json +144 -0
- package/src/__fixtures__/compose/three-net.plan.json +174 -0
- package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
- package/src/__fixtures__/compose/working-after-apply.json +145 -0
- package/src/__fixtures__/config-blocks/app/main.tf +61 -0
- package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
- package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
- package/src/__fixtures__/config-files/shadow/main.tf +3 -0
- package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
- package/src/__fixtures__/config-files/shadow/other.tf +3 -0
- package/src/__fixtures__/config-files/tf/main.tf +11 -0
- package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
- package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
- package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
- package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
- package/src/__fixtures__/drift-record/README.md +11 -0
- package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
- package/src/__fixtures__/instance-forms/app/main.tf +110 -0
- package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
- package/src/__fixtures__/instance-vars/app/main.tf +52 -0
- package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
- package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
- package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
- package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
- package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
- package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
- package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
- package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
- package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
- package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
- package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
- package/src/__fixtures__/pr-loop/README.md +50 -0
- package/src/__fixtures__/pr-loop/chant.config.json +13 -0
- package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
- package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
- package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
- package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
- package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
- package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
- package/src/__fixtures__/report/applied.show.json +51 -0
- package/src/__fixtures__/report/generate.ts +63 -0
- package/src/__fixtures__/report/plan.show.json +200 -0
- package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
- package/src/__fixtures__/report/tf-apply/report.json +338 -0
- package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
- package/src/__fixtures__/report/tf-plan/report.json +320 -0
- package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
- package/src/applied-state.ts +52 -0
- package/src/backend-config.test.ts +105 -0
- package/src/backend-config.ts +133 -0
- package/src/behaviour/kinds.ts +4 -0
- package/src/behaviour/predict.ts +5 -2
- package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
- package/src/behaviour/reference-catalog.ts +16 -5
- package/src/behaviour/request.ts +3 -2
- package/src/change-set-instances.test.ts +98 -0
- package/src/changed-roots.ts +14 -16
- package/src/commands.ts +84 -4
- package/src/components/capability-plugin.test.ts +172 -5
- package/src/components/capability-plugin.ts +212 -11
- package/src/components/change-set-graph.e2e.test.ts +11 -5
- package/src/components/compose-group.test.ts +486 -0
- package/src/components/compose-group.ts +489 -0
- package/src/components/demo-composed-roots.e2e.test.ts +440 -0
- package/src/components/member-outputs.test.ts +76 -0
- package/src/components/placeholder-preview.test.ts +147 -0
- package/src/components/placeholder-preview.ts +257 -0
- package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
- package/src/components/pr-loop.tofu.test.ts +112 -56
- package/src/compose/apply-events.test.ts +62 -0
- package/src/compose/apply-events.ts +100 -0
- package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
- package/src/compose/estate.test.ts +421 -0
- package/src/compose/estate.ts +443 -0
- package/src/compose/hcl.ts +311 -0
- package/src/compose/module-address.ts +49 -0
- package/src/compose/root.test.ts +416 -0
- package/src/compose/root.ts +592 -0
- package/src/compose/run.test.ts +440 -0
- package/src/compose/run.tofu.test.ts +176 -0
- package/src/compose/run.ts +527 -0
- package/src/compose/slice.test.ts +184 -0
- package/src/compose/slice.ts +290 -0
- package/src/compose/state-address.ts +157 -0
- package/src/compose/state-merge.test.ts +131 -0
- package/src/compose/state-merge.ts +148 -0
- package/src/compose.test.ts +248 -0
- package/src/compose.ts +627 -0
- package/src/composites/terraform-apply-op.test.ts +9 -0
- package/src/composites/terraform-apply-op.ts +7 -3
- package/src/composites/terraform-pin-rollout-op.test.ts +2 -2
- package/src/config-file-listers.test.ts +265 -0
- package/src/config.ts +44 -9
- package/src/describe-resources.engine-facts.test.ts +412 -0
- package/src/describe-resources.live-module-keyed.test.ts +290 -0
- package/src/describe-resources.ts +416 -70
- package/src/drift-read.ts +284 -0
- package/src/drift-record.test.ts +223 -0
- package/src/drift-record.ts +455 -0
- package/src/hcl/config-blocks.test.ts +174 -0
- package/src/hcl/edges.ts +21 -1
- package/src/hcl/files.ts +234 -0
- package/src/hcl/instance-forms.test.ts +94 -0
- package/src/hcl/instances.test.ts +137 -0
- package/src/hcl/instances.ts +422 -0
- package/src/hcl/parse.test.ts +139 -1
- package/src/hcl/parse.ts +149 -16
- package/src/hcl/roots.ts +6 -0
- package/src/hcl/suppressions.ts +8 -2
- package/src/index.ts +76 -0
- package/src/live-plan-reasons.ts +54 -0
- package/src/lsp/completions.test.ts +1 -1
- package/src/lsp/option-keys.ts +2 -0
- package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
- package/src/op/activities/index.ts +9 -0
- package/src/op/activities/live-detect.ts +55 -26
- package/src/op/activities/pin-rollout.ts +3 -0
- package/src/op/activities/registry.test.ts +2 -1
- package/src/op/activities/terraform.test.ts +55 -0
- package/src/op/activities/terraform.ts +266 -22
- package/src/op/activity-contracts.test.ts +1 -0
- package/src/op/activity-contracts.ts +24 -1
- package/src/op/builders.ts +14 -0
- package/src/pin/bundle.test.ts +2 -0
- package/src/pin/edit.ts +30 -0
- package/src/pin/rollout.ts +16 -11
- package/src/plan-shape.test.ts +148 -0
- package/src/plan-shape.ts +128 -0
- package/src/plugin.ts +33 -19
- package/src/report.test.ts +423 -0
- package/src/report.ts +752 -0
- package/src/signal-only-root.test.ts +72 -0
- package/src/skills/chant-terraform.md +1 -1
- package/src/state/azure-blob.ts +223 -0
- package/src/state/backend-verdict.test.ts +187 -0
- package/src/state/backend.test.ts +127 -0
- package/src/state/backend.ts +488 -0
- package/src/state/exec.ts +111 -0
- package/src/state/gcs.ts +254 -0
- package/src/state/index.ts +15 -0
- package/src/state/locks.test.ts +84 -0
- package/src/state/locks.ts +201 -0
- package/src/state/object-store.ts +35 -0
- package/src/state/root-states-floci.e2e.test.ts +66 -0
- package/src/state/root-states.test.ts +462 -0
- package/src/state/root-states.ts +309 -0
- package/src/state/s3.ts +193 -0
- package/src/state/store.ts +225 -0
- package/src/terragrunt/affected.ts +2 -1
- package/src/workspace-kinds.test.ts +4 -17
- package/workspace-kinds.json +1 -24
- package/dist/workspace-member.d.ts +0 -30
- package/dist/workspace-member.d.ts.map +0 -1
- package/dist/workspace-member.js +0 -91
- package/schemas/pulumi.schema.json +0 -34
- package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
- package/src/testdata/pulumi-member/stack-export.json +0 -53
- package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
- package/src/workspace-member.test.ts +0 -89
- package/src/workspace-member.ts +0 -126
|
@@ -45,12 +45,14 @@ import { tmpdir } from "node:os";
|
|
|
45
45
|
import { promisify } from "node:util";
|
|
46
46
|
import { resolve, dirname, join, relative, sep } from "node:path";
|
|
47
47
|
import { loadChantConfigUpward } from "@intentius/chant/config";
|
|
48
|
+
import { findWorkspaceRoot } from "@intentius/chant/project-root";
|
|
48
49
|
import { liveReadKey, memoLiveRead } from "@intentius/chant/live-read-session";
|
|
49
50
|
import { terraformPlanDigest } from "../../plan-digest";
|
|
50
51
|
import { choudoufuSetPlanParts, plannerForBinary, terraformChangeSetPart } from "../../change-set";
|
|
51
52
|
import type { ChangeSetPart } from "@intentius/chant/change-set";
|
|
52
53
|
import type { TerraformConfig, TerraformRootConfig } from "../../config";
|
|
53
54
|
import { detectLiveEstate } from "./live-detect";
|
|
55
|
+
import { backendConfigValues } from "../../backend-config";
|
|
54
56
|
import {
|
|
55
57
|
parseAdoptionCommands,
|
|
56
58
|
readAdoptionLedger,
|
|
@@ -116,6 +118,36 @@ export const MIN_CHOUDOUFU_VERSION = "0.15.0";
|
|
|
116
118
|
*/
|
|
117
119
|
export const MIN_CHOUDOUFU_WAVES_VERSION = "0.22.0";
|
|
118
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The choudoufu release a composed run of estates needs (#4445): a root with
|
|
123
|
+
* no `live` block that calls estate directories as modules, planned and
|
|
124
|
+
* applied as one walk, with a `terraform_estate_outputs` read of another
|
|
125
|
+
* estate in the run answered inside it (choudoufu#2059, first released in
|
|
126
|
+
* v0.31.0). Only the composed estate run (`compose/estate.ts`) checks it;
|
|
127
|
+
* below it the members plan wave by wave. A dev build names no release and
|
|
128
|
+
* passes, as for the other floors.
|
|
129
|
+
*/
|
|
130
|
+
export const MIN_CHOUDOUFU_ESTATES_VERSION = "0.31.0";
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The choudoufu release whose `live-graph -json -from-plan=<file>` prints the
|
|
134
|
+
* engine graph document (chant#4520's design section 4, choudoufu#2285), the
|
|
135
|
+
* one `describeResources()` reads engine facts from on a live root
|
|
136
|
+
* (chant#4528). choudoufu's changelog lists the command under
|
|
137
|
+
* `v0.32.0 (Unreleased)`, so this is the version it releases as. The chant
|
|
138
|
+
* side of the same contract, the first chant that writes those facts, is
|
|
139
|
+
* `ENGINE_FACTS_FLOOR` in core's engine-facts.ts, which is NEXT_RELEASE
|
|
140
|
+
* until the cut.
|
|
141
|
+
*
|
|
142
|
+
* Not a refusal. Facts are for people and never feed a decision, so a
|
|
143
|
+
* choudoufu without the command (a usage error, see
|
|
144
|
+
* {@link classifyLiveGraphFailure}) leaves `facts` absent with one note
|
|
145
|
+
* naming this version, and the rest of the read is unchanged. The check is
|
|
146
|
+
* the capability, not the version string: a local or dev build names a
|
|
147
|
+
* version below this or none at all, and still carries the command.
|
|
148
|
+
*/
|
|
149
|
+
export const MIN_CHOUDOUFU_GRAPH_VERSION = "0.32.0";
|
|
150
|
+
|
|
119
151
|
/**
|
|
120
152
|
* Where a choudoufu root's wave files live, relative to the project root:
|
|
121
153
|
* `<root>/plan-set.json` (the set plan document chant planned and the
|
|
@@ -191,6 +223,14 @@ export interface TerraformPlanArgs extends TerraformRootArgs {
|
|
|
191
223
|
* of that file changes nothing else.
|
|
192
224
|
*/
|
|
193
225
|
targets?: string[];
|
|
226
|
+
/**
|
|
227
|
+
* `false` skips `show -no-color`, the human-readable render (#4479). The
|
|
228
|
+
* plan then runs one `show` process, `show -json`, whose document gives the
|
|
229
|
+
* digest, the change set and everything a caller derives from `json`, and
|
|
230
|
+
* {@link TerraformPlanResult.text} is `""`. For a caller that never reads
|
|
231
|
+
* the text, such as the `terraform-apply` capability. Default: `true`.
|
|
232
|
+
*/
|
|
233
|
+
text?: boolean;
|
|
194
234
|
}
|
|
195
235
|
|
|
196
236
|
export interface TerraformApplyArgs extends TerraformRootArgs {
|
|
@@ -280,6 +320,13 @@ export interface TerraformInitResult {
|
|
|
280
320
|
dir: string;
|
|
281
321
|
/** Workspace the run selected via `TF_WORKSPACE`, when the root names one. */
|
|
282
322
|
workspace?: string;
|
|
323
|
+
/**
|
|
324
|
+
* The root module directory relative to the chant workspace root, with `/`
|
|
325
|
+
* separators (chant#4482), when the root sits inside a workspace. A
|
|
326
|
+
* TerraformApplyOp publishes it as the `TerraformRootPath` run outcome, so a
|
|
327
|
+
* ledger read can name the member whose directory holds the root it applied.
|
|
328
|
+
*/
|
|
329
|
+
rootPath?: string;
|
|
283
330
|
}
|
|
284
331
|
|
|
285
332
|
/** Counts projected out of a plan's `resource_changes`. */
|
|
@@ -304,7 +351,7 @@ export interface TerraformPlanResult extends PlanChangeCounts {
|
|
|
304
351
|
dir: string;
|
|
305
352
|
/** `terraform show -json <planFile>`, parsed. */
|
|
306
353
|
json: unknown;
|
|
307
|
-
/** `terraform show -no-color <planFile>` — the human-readable plan. */
|
|
354
|
+
/** `terraform show -no-color <planFile>` — the human-readable plan. `""` when the plan ran with `text: false`. */
|
|
308
355
|
text: string;
|
|
309
356
|
/**
|
|
310
357
|
* This plan's identity (#2300) — {@link terraformPlanDigest} over
|
|
@@ -537,19 +584,24 @@ export function terraformEnvironment(root?: Pick<TerraformRootConfig, "workspace
|
|
|
537
584
|
};
|
|
538
585
|
}
|
|
539
586
|
|
|
540
|
-
/**
|
|
587
|
+
/**
|
|
588
|
+
* `terraform init`, with the root's `backendConfigFiles` as
|
|
589
|
+
* `-backend-config=<path>` flags and then its `backendConfig` as
|
|
590
|
+
* `-backend-config=k=v` flags (chant#4459): init applies later flags over
|
|
591
|
+
* earlier ones, so a pair overrides a file. A path is passed as written; init
|
|
592
|
+
* runs in the root's directory, which a relative one resolves against.
|
|
593
|
+
*/
|
|
541
594
|
export function terraformInitCommand(opts: {
|
|
542
595
|
binary: string;
|
|
543
596
|
backendConfig?: Record<string, string>;
|
|
597
|
+
backendConfigFiles?: readonly string[];
|
|
544
598
|
upgrade?: boolean;
|
|
545
599
|
reconfigure?: boolean;
|
|
546
600
|
}): string {
|
|
547
601
|
const parts = [opts.binary, "init", "-input=false"];
|
|
548
602
|
if (opts.upgrade) parts.push("-upgrade");
|
|
549
603
|
if (opts.reconfigure) parts.push("-reconfigure");
|
|
550
|
-
for (const
|
|
551
|
-
parts.push(`-backend-config=${quoteArg(`${key}=${value}`)}`);
|
|
552
|
-
}
|
|
604
|
+
for (const value of backendConfigValues(opts)) parts.push(`-backend-config=${quoteArg(value)}`);
|
|
553
605
|
return parts.join(" ");
|
|
554
606
|
}
|
|
555
607
|
|
|
@@ -629,6 +681,37 @@ export function choudoufuLivePlanCommand(opts: {
|
|
|
629
681
|
return parts.join(" ");
|
|
630
682
|
}
|
|
631
683
|
|
|
684
|
+
/**
|
|
685
|
+
* `live-graph -json -from-plan=<file> [-estate=<estate>]` (choudoufu#2285).
|
|
686
|
+
* No `DIR` argument: like {@link choudoufuLiveCheckCommand}, the activity
|
|
687
|
+
* runs in the root directory, which is DIR's default. `-estate` follows
|
|
688
|
+
* {@link choudoufuLivePlanCommand}'s rule: only for a root that does not
|
|
689
|
+
* name its own estate. `-history` is never passed: record versions take a
|
|
690
|
+
* listing per record and `s3:ListBucketVersions`, and an observation read
|
|
691
|
+
* has no use for them (the #4520 ruling, Q6: opt-in).
|
|
692
|
+
*/
|
|
693
|
+
export function choudoufuLiveGraphCommand(opts: { binary: string; fromPlan: string; estate?: string }): string {
|
|
694
|
+
const parts = [opts.binary, "live-graph", "-json", `-from-plan=${quoteArg(opts.fromPlan)}`];
|
|
695
|
+
if (opts.estate !== undefined) parts.push(`-estate=${quoteArg(opts.estate)}`);
|
|
696
|
+
return parts.join(" ");
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Whether a failed `live-graph` run is a choudoufu that has no such command
|
|
701
|
+
* or flag (`"unsupported"`), or a real failure (`"failed"`). Older builds
|
|
702
|
+
* answer an unknown command two ways: the new CLI prints `OpenTofu has no
|
|
703
|
+
* command named "live-graph"` and exits 1 (cmd/choudoufu/command_main.go),
|
|
704
|
+
* and the older mitchellh/cli path prints its usage and exits 127. A build
|
|
705
|
+
* whose `live-graph` predates `-from-plan` fails the flag parse with `flag
|
|
706
|
+
* provided but not defined`.
|
|
707
|
+
*/
|
|
708
|
+
export function classifyLiveGraphFailure(code: unknown, output: string): "unsupported" | "failed" {
|
|
709
|
+
if (code === 127) return "unsupported";
|
|
710
|
+
if (/no command named "?live-graph"?/i.test(output)) return "unsupported";
|
|
711
|
+
if (/flag provided but not defined: -(?:from-plan|json|estate)\b/.test(output)) return "unsupported";
|
|
712
|
+
return "failed";
|
|
713
|
+
}
|
|
714
|
+
|
|
632
715
|
/** `live-ls -estate=<estate> -json [-consistent]`. */
|
|
633
716
|
export function choudoufuLiveLsCommand(opts: { binary: string; estate: string; consistent?: boolean }): string {
|
|
634
717
|
const parts = [opts.binary, "live-ls", `-estate=${quoteArg(opts.estate)}`, "-json"];
|
|
@@ -917,8 +1000,8 @@ function report(stdout: string, stderr: string): void {
|
|
|
917
1000
|
// ── Activities ──────────────────────────────────────────────────────────────
|
|
918
1001
|
|
|
919
1002
|
/**
|
|
920
|
-
* `terraform init` in the named root, with the root's `
|
|
921
|
-
* as `-backend-config` flags. Uses the longInfra profile: init downloads
|
|
1003
|
+
* `terraform init` in the named root, with the root's `backendConfigFiles` and
|
|
1004
|
+
* `backendConfig` supplied as `-backend-config` flags. Uses the longInfra profile: init downloads
|
|
922
1005
|
* providers and modules, a network call of unpredictable size.
|
|
923
1006
|
*/
|
|
924
1007
|
export async function terraformInit(
|
|
@@ -929,19 +1012,55 @@ export async function terraformInit(
|
|
|
929
1012
|
const cmd = terraformInitCommand({
|
|
930
1013
|
binary,
|
|
931
1014
|
...(root.backendConfig ? { backendConfig: root.backendConfig } : {}),
|
|
1015
|
+
...(root.backendConfigFiles ? { backendConfigFiles: root.backendConfigFiles } : {}),
|
|
932
1016
|
...(args.upgrade ? { upgrade: true } : {}),
|
|
933
1017
|
...(args.reconfigure ? { reconfigure: true } : {}),
|
|
934
1018
|
});
|
|
935
1019
|
|
|
936
|
-
|
|
937
|
-
|
|
1020
|
+
// Known once the root resolves, before init runs (chant#4512), so a failed
|
|
1021
|
+
// init still publishes them and the ledger names the root it was for.
|
|
1022
|
+
const rootPath = workspaceRelativePath(dir);
|
|
1023
|
+
const result: TerraformInitResult = { dir, ...(root.workspace ? { workspace: root.workspace } : {}), ...(rootPath !== undefined ? { rootPath } : {}) };
|
|
1024
|
+
|
|
1025
|
+
let out: { stdout: string; stderr: string };
|
|
1026
|
+
try {
|
|
1027
|
+
out = await run(cmd, dir, terraformEnvironment(root), signal);
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
throw withKnownOutputs(err, result);
|
|
1030
|
+
}
|
|
1031
|
+
report(out.stdout, out.stderr);
|
|
1032
|
+
return result;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* `err` carrying `outputs` as the outputs the activity knew when it failed
|
|
1037
|
+
* (chant#4512): the `knownOutputs` property core's executor reads a failed
|
|
1038
|
+
* step's outcome attributes from (core's `withKnownOutputs`). It is set here
|
|
1039
|
+
* rather than imported so the lexicon needs no newer core; an older executor
|
|
1040
|
+
* reads the error as any other failure.
|
|
1041
|
+
*/
|
|
1042
|
+
function withKnownOutputs(err: unknown, outputs: object): Error {
|
|
1043
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1044
|
+
return Object.assign(e, { knownOutputs: outputs });
|
|
1045
|
+
}
|
|
938
1046
|
|
|
939
|
-
|
|
1047
|
+
/**
|
|
1048
|
+
* `dir` relative to the chant workspace that holds it, `/`-separated, or
|
|
1049
|
+
* undefined outside a workspace or outside its root (chant#4482). The
|
|
1050
|
+
* workspace root `"."` stands for itself.
|
|
1051
|
+
*/
|
|
1052
|
+
export function workspaceRelativePath(dir: string): string | undefined {
|
|
1053
|
+
const found = findWorkspaceRoot(dir);
|
|
1054
|
+
if (!found) return undefined;
|
|
1055
|
+
const rel = relative(found.dir, dir);
|
|
1056
|
+
if (rel.startsWith("..")) return undefined;
|
|
1057
|
+
return rel === "" ? "." : rel.split(sep).join("/");
|
|
940
1058
|
}
|
|
941
1059
|
|
|
942
1060
|
/**
|
|
943
1061
|
* `terraform plan -detailed-exitcode -out=<planFile>` in the named root, then
|
|
944
|
-
* `terraform show` over the written plan in both `-json` and `-no-color` form
|
|
1062
|
+
* `terraform show` over the written plan in both `-json` and `-no-color` form
|
|
1063
|
+
* (`-json` alone with `text: false`).
|
|
945
1064
|
*
|
|
946
1065
|
* The exit code is the answer, not an error condition: 0 is a plan with no
|
|
947
1066
|
* changes, 2 is a plan with changes, and anything else, 1 included, is a
|
|
@@ -989,7 +1108,7 @@ export async function terraformPlan(
|
|
|
989
1108
|
}
|
|
990
1109
|
|
|
991
1110
|
const jsonRun = await run(terraformShowCommand({ binary, json: true, planFile }), dir, env, signal);
|
|
992
|
-
const
|
|
1111
|
+
const text = args.text === false ? "" : (await run(terraformShowCommand({ binary, json: false, planFile }), dir, env, signal)).stdout;
|
|
993
1112
|
const json: unknown = JSON.parse(jsonRun.stdout);
|
|
994
1113
|
|
|
995
1114
|
return {
|
|
@@ -997,7 +1116,7 @@ export async function terraformPlan(
|
|
|
997
1116
|
planFile,
|
|
998
1117
|
dir,
|
|
999
1118
|
json,
|
|
1000
|
-
text
|
|
1119
|
+
text,
|
|
1001
1120
|
planDigest: terraformPlanDigest(json),
|
|
1002
1121
|
changeSet: terraformChangeSetPart({
|
|
1003
1122
|
member: args.root,
|
|
@@ -1092,7 +1211,11 @@ export async function terraformOutputs(
|
|
|
1092
1211
|
args: TerraformRootArgs,
|
|
1093
1212
|
signal?: AbortSignal,
|
|
1094
1213
|
): Promise<Record<string, unknown>> {
|
|
1095
|
-
|
|
1214
|
+
return outputsIn(await resolveRoot(args, signal), signal);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/** `<binary> output -json` in a resolved root, as `{ name: value }`. */
|
|
1218
|
+
async function outputsIn({ binary, root, dir }: ResolvedRoot, signal?: AbortSignal): Promise<Record<string, unknown>> {
|
|
1096
1219
|
const { stdout } = await run(`${binary} output -json`, dir, terraformEnvironment(root), signal);
|
|
1097
1220
|
const parsed = JSON.parse(stdout || "{}") as Record<string, { value?: unknown }>;
|
|
1098
1221
|
return Object.fromEntries(Object.entries(parsed).map(([name, entry]) => [name, entry?.value]));
|
|
@@ -1174,7 +1297,7 @@ export async function choudoufuWaveRoot(
|
|
|
1174
1297
|
if (resolved.root.varFiles && resolved.root.varFiles.length > 0) {
|
|
1175
1298
|
return { wave: false, reason: "the root has var files, which live-plan-set does not take" };
|
|
1176
1299
|
}
|
|
1177
|
-
if (resolved.root.backendConfig && Object.keys(resolved.root.backendConfig).length > 0) {
|
|
1300
|
+
if ((resolved.root.backendConfig && Object.keys(resolved.root.backendConfig).length > 0) || (resolved.root.backendConfigFiles?.length ?? 0) > 0) {
|
|
1178
1301
|
return { wave: false, reason: "the root has backend config, which live-plan-set does not take" };
|
|
1179
1302
|
}
|
|
1180
1303
|
const root = choudoufuSetRootPath(resolved.projectRoot, resolved.dir);
|
|
@@ -1275,13 +1398,17 @@ export interface ChoudoufuPlanSetResult extends ChoudoufuPlanSet {
|
|
|
1275
1398
|
* `live-wave-apply` hands back to choudoufu with the set digest.
|
|
1276
1399
|
*/
|
|
1277
1400
|
export async function choudoufuPlanSet(args: TerraformRootArgs, signal?: AbortSignal): Promise<ChoudoufuPlanSetResult> {
|
|
1278
|
-
|
|
1401
|
+
return planSetIn(await resolveRoot(args, signal), args.root, signal);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/** {@link choudoufuPlanSet} over a resolved root, `key` naming it in errors, in the waves directory and as the change-set member. */
|
|
1405
|
+
async function planSetIn(resolved: ResolvedRoot, key: string, signal?: AbortSignal): Promise<ChoudoufuPlanSetResult> {
|
|
1279
1406
|
const { binary, root, dir, projectRoot } = resolved;
|
|
1280
|
-
if (binary !== "choudoufu") throw new Error(`choudoufuPlanSet: root "${
|
|
1407
|
+
if (binary !== "choudoufu") throw new Error(`choudoufuPlanSet: root "${key}" runs ${binary}, not choudoufu`);
|
|
1281
1408
|
await ensureChoudoufuWavesVersion(binary, signal);
|
|
1282
1409
|
const rootPath = choudoufuSetRootPath(projectRoot, dir);
|
|
1283
|
-
if (rootPath === undefined) throw new Error(`choudoufuPlanSet: root "${
|
|
1284
|
-
const wavesDir = choudoufuWavesDir(projectRoot,
|
|
1410
|
+
if (rootPath === undefined) throw new Error(`choudoufuPlanSet: root "${key}" (${dir}) is not inside ${projectRoot}`);
|
|
1411
|
+
const wavesDir = choudoufuWavesDir(projectRoot, key);
|
|
1285
1412
|
mkdirSync(wavesDir, { recursive: true });
|
|
1286
1413
|
const cmd = choudoufuPlanSetCommand({ binary, root: rootPath, outDir: fromProject(projectRoot, join(wavesDir, "out")) });
|
|
1287
1414
|
|
|
@@ -1299,12 +1426,64 @@ export async function choudoufuPlanSet(args: TerraformRootArgs, signal?: AbortSi
|
|
|
1299
1426
|
stdout = failure.stdout ?? "";
|
|
1300
1427
|
}
|
|
1301
1428
|
|
|
1302
|
-
const parsed = parseChoudoufuPlanSet(stdout, { member:
|
|
1429
|
+
const parsed = parseChoudoufuPlanSet(stdout, { member: key, root: rootPath });
|
|
1303
1430
|
const planSetFile = join(wavesDir, "plan-set.json");
|
|
1304
1431
|
writeFileSync(planSetFile, stdout);
|
|
1305
1432
|
return { ...parsed, planSetFile, root: rootPath };
|
|
1306
1433
|
}
|
|
1307
1434
|
|
|
1435
|
+
// ── A choudoufu workspace member's outputs (#4455) ─────────────────────────
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* A choudoufu member of the workspace, as `member-outputs` names it: not a
|
|
1439
|
+
* `terraform.roots` entry of the reading project but a directory the
|
|
1440
|
+
* workspace declaration gives. Its binary is `choudoufu`, it has no var
|
|
1441
|
+
* files, workspace or backend config, and it runs from `projectRoot`, a
|
|
1442
|
+
* directory above it (the workspace root), since `live-plan-set` plans only
|
|
1443
|
+
* roots inside the directory it runs in.
|
|
1444
|
+
*/
|
|
1445
|
+
export interface ChoudoufuMemberArgs {
|
|
1446
|
+
/** The member's name: the waves directory's name, and the plan's member in errors. */
|
|
1447
|
+
member: string;
|
|
1448
|
+
/** The member's directory, absolute. */
|
|
1449
|
+
dir: string;
|
|
1450
|
+
/** Where `live-plan-set` runs from, absolute: a directory above `dir`. */
|
|
1451
|
+
projectRoot: string;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
async function resolveMemberRoot(args: ChoudoufuMemberArgs, signal?: AbortSignal): Promise<ResolvedRoot> {
|
|
1455
|
+
const binary = "choudoufu";
|
|
1456
|
+
await ensureChoudoufuVersion(binary, signal);
|
|
1457
|
+
const dir = resolve(args.dir);
|
|
1458
|
+
const estate = detectLiveEstate(dir);
|
|
1459
|
+
return { binary, root: { dir }, dir, ...(estate !== undefined ? { estate } : {}), live: estate !== undefined, projectRoot: resolve(args.projectRoot) };
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* The member's outputs as applied (#4455): `choudoufu init`, then `choudoufu
|
|
1464
|
+
* output -json` in its directory, the read the `terraform-apply` capability's
|
|
1465
|
+
* `outputs` makes for a root. A live estate's outputs come from its record
|
|
1466
|
+
* store.
|
|
1467
|
+
*/
|
|
1468
|
+
export async function choudoufuMemberOutputs(args: ChoudoufuMemberArgs, signal?: AbortSignal): Promise<Record<string, unknown>> {
|
|
1469
|
+
const resolved = await resolveMemberRoot(args, signal);
|
|
1470
|
+
const { stdout, stderr } = await run(terraformInitCommand({ binary: resolved.binary }), resolved.dir, terraformEnvironment(resolved.root), signal);
|
|
1471
|
+
report(stdout, stderr);
|
|
1472
|
+
return outputsIn(resolved, signal);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* The member planned as a one-root set (#4455), the way a choudoufu root in
|
|
1477
|
+
* a gated wave is ({@link choudoufuPlanSet}): `live-plan-set -json` from
|
|
1478
|
+
* `projectRoot`. The document's embedded plan carries `planned_values` and
|
|
1479
|
+
* `output_changes`, which say what the member's outputs will be.
|
|
1480
|
+
*/
|
|
1481
|
+
export async function choudoufuMemberPlanSet(args: ChoudoufuMemberArgs, signal?: AbortSignal): Promise<ChoudoufuPlanSetResult> {
|
|
1482
|
+
const resolved = await resolveMemberRoot(args, signal);
|
|
1483
|
+
if (!resolved.live) throw new Error(`choudoufu member "${args.member}" (${resolved.dir}) declares no live estate, so live-plan-set cannot plan it`);
|
|
1484
|
+
return planSetIn(resolved, args.member, signal);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1308
1487
|
/**
|
|
1309
1488
|
* What a choudoufu root keeps in the fan-out's attempt record (#3459):
|
|
1310
1489
|
* choudoufu's resume file, under the set digest it was written for.
|
|
@@ -1514,10 +1693,11 @@ export function terraformPlanReadCommand(opts: { binary: string; planFile: strin
|
|
|
1514
1693
|
* The one line a person runs to initialise a root chant refuses to plan,
|
|
1515
1694
|
* with the root's `backendConfig` carried, so it can be pasted as printed.
|
|
1516
1695
|
*/
|
|
1517
|
-
export function terraformInitLine(opts: { binary: string; dir: string; backendConfig?: Record<string, string
|
|
1696
|
+
export function terraformInitLine(opts: { binary: string; dir: string; backendConfig?: Record<string, string>; backendConfigFiles?: readonly string[] }): string {
|
|
1518
1697
|
const init = terraformInitCommand({
|
|
1519
1698
|
binary: opts.binary,
|
|
1520
1699
|
...(opts.backendConfig ? { backendConfig: opts.backendConfig } : {}),
|
|
1700
|
+
...(opts.backendConfigFiles ? { backendConfigFiles: opts.backendConfigFiles } : {}),
|
|
1521
1701
|
});
|
|
1522
1702
|
return init.replace(`${opts.binary} init`, `${opts.binary} -chdir=${quoteArg(opts.dir)} init`);
|
|
1523
1703
|
}
|
|
@@ -1629,7 +1809,12 @@ export async function terraformPlanRead(
|
|
|
1629
1809
|
): Promise<TerraformPlanReadResult> {
|
|
1630
1810
|
const { binary, root, dir } = await resolveRoot(args, signal);
|
|
1631
1811
|
const env = terraformEnvironment(root);
|
|
1632
|
-
const initLine = terraformInitLine({
|
|
1812
|
+
const initLine = terraformInitLine({
|
|
1813
|
+
binary,
|
|
1814
|
+
dir,
|
|
1815
|
+
...(root.backendConfig ? { backendConfig: root.backendConfig } : {}),
|
|
1816
|
+
...(root.backendConfigFiles ? { backendConfigFiles: root.backendConfigFiles } : {}),
|
|
1817
|
+
});
|
|
1633
1818
|
const notInitialised = (detail: string): TerraformPlanReadError =>
|
|
1634
1819
|
new TerraformPlanReadError(
|
|
1635
1820
|
"not-initialised",
|
|
@@ -1912,6 +2097,65 @@ export async function choudoufuLivePlan(
|
|
|
1912
2097
|
};
|
|
1913
2098
|
}
|
|
1914
2099
|
|
|
2100
|
+
/** What {@link choudoufuLiveGraph} resolved. */
|
|
2101
|
+
export type ChoudoufuLiveGraphResult =
|
|
2102
|
+
| {
|
|
2103
|
+
/** choudoufu printed the engine graph document. */
|
|
2104
|
+
supported: true;
|
|
2105
|
+
/** The document, parsed: a GraphIR v1 with `meta.engineGraph` (chant#4527's schema). */
|
|
2106
|
+
json: unknown;
|
|
2107
|
+
/** Absolute path of the root module directory. */
|
|
2108
|
+
dir: string;
|
|
2109
|
+
}
|
|
2110
|
+
| {
|
|
2111
|
+
/** This choudoufu has no `live-graph -from-plan` (older than {@link MIN_CHOUDOUFU_GRAPH_VERSION}). */
|
|
2112
|
+
supported: false;
|
|
2113
|
+
/** choudoufu's own first line, for the note. */
|
|
2114
|
+
detail: string;
|
|
2115
|
+
dir: string;
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2118
|
+
export interface ChoudoufuLiveGraphArgs extends TerraformRootArgs {
|
|
2119
|
+
/** Absolute path of the `live-plan -json` document this graph takes its facts from. */
|
|
2120
|
+
fromPlan: string;
|
|
2121
|
+
/** The estate (`-estate`), for a root that does not declare its own. */
|
|
2122
|
+
estate?: string;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
/**
|
|
2126
|
+
* `choudoufu live-graph -json -from-plan=<file>` (choudoufu#2285, read by
|
|
2127
|
+
* chant#4528): the root's graph as one engine graph document, its facts taken
|
|
2128
|
+
* from a `live-plan -json` document this process already read. No second live
|
|
2129
|
+
* read: choudoufu builds the graph from the configuration and that file
|
|
2130
|
+
* alone, starts no provider and calls no cloud, so facts and verdicts come
|
|
2131
|
+
* from the same refresh (choudoufu #1014).
|
|
2132
|
+
*
|
|
2133
|
+
* A usage error ({@link classifyLiveGraphFailure}) resolves `supported:
|
|
2134
|
+
* false` rather than throwing, since the caller degrades to no facts; any
|
|
2135
|
+
* other failure throws with choudoufu's output. Warnings (an empty scope
|
|
2136
|
+
* estate, a plan without `observed_at`) go to stderr and are echoed like
|
|
2137
|
+
* every other activity's.
|
|
2138
|
+
*/
|
|
2139
|
+
export async function choudoufuLiveGraph(args: ChoudoufuLiveGraphArgs, signal?: AbortSignal): Promise<ChoudoufuLiveGraphResult> {
|
|
2140
|
+
const resolved = await resolveRoot(args, signal);
|
|
2141
|
+
const { binary, dir } = resolved;
|
|
2142
|
+
const env = terraformEnvironment(resolved.root);
|
|
2143
|
+
const estateFlag: { estate?: string } = resolved.estate === undefined && args.estate !== undefined ? { estate: args.estate } : {};
|
|
2144
|
+
const cmd = choudoufuLiveGraphCommand({ binary, fromPlan: args.fromPlan, ...estateFlag });
|
|
2145
|
+
try {
|
|
2146
|
+
const { stdout, stderr } = await run(cmd, dir, env, signal);
|
|
2147
|
+
if (stderr) console.error(stderr);
|
|
2148
|
+
return { supported: true, json: JSON.parse(liveDocumentFrom(stdout)), dir };
|
|
2149
|
+
} catch (err) {
|
|
2150
|
+
const failure = err as ExecFailure;
|
|
2151
|
+
if (typeof failure.code !== "number") throw err;
|
|
2152
|
+
const output = `${failure.stderr ?? ""}\n${failure.stdout ?? ""}`;
|
|
2153
|
+
const detail = output.trim().split("\n").find((line) => line.trim() !== "")?.trim() ?? `exit ${failure.code}`;
|
|
2154
|
+
if (classifyLiveGraphFailure(failure.code, output) === "unsupported") return { supported: false, detail, dir };
|
|
2155
|
+
throw new Error(`${binary} live-graph failed in ${dir} (exit ${failure.code})${detail ? `\n${output.trim()}` : ""}`);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
1915
2159
|
/**
|
|
1916
2160
|
* `choudoufu live-ls -estate=<estate> -json [-consistent]`: every resource
|
|
1917
2161
|
* the account holds under the estate, read straight from the Resource Groups
|
|
@@ -66,7 +66,8 @@ export const terraformInitContract = activityContract(
|
|
|
66
66
|
upgrade: z.boolean().optional(),
|
|
67
67
|
reconfigure: z.boolean().optional(),
|
|
68
68
|
}),
|
|
69
|
-
|
|
69
|
+
// `rootPath` (#4482) is what TerraformApplyOp's Init step publishes as `TerraformRootPath`; OPS012 checks it here.
|
|
70
|
+
z.object({ dir: z.string(), workspace: z.string().optional(), rootPath: z.string().optional() }),
|
|
70
71
|
rootEntity,
|
|
71
72
|
);
|
|
72
73
|
|
|
@@ -181,6 +182,28 @@ export const choudoufuAdoptContract = activityContract(
|
|
|
181
182
|
rootEntity,
|
|
182
183
|
);
|
|
183
184
|
|
|
185
|
+
/**
|
|
186
|
+
* `choudoufuPlanSet` (#4483): one live choudoufu root planned as a one-root
|
|
187
|
+
* set. `changeSet` is the root's part, the field an Op hands to
|
|
188
|
+
* `composeChangeSet`; `planDigest` is what a gate binds; `planSetFile` and
|
|
189
|
+
* `setDigest` are what a later `live-wave-apply` takes.
|
|
190
|
+
*/
|
|
191
|
+
export const choudoufuPlanSetContract = activityContract(
|
|
192
|
+
"choudoufuPlanSet",
|
|
193
|
+
z.strictObject({ ...rootArgs }),
|
|
194
|
+
z.object({
|
|
195
|
+
setDigest: z.string(),
|
|
196
|
+
rootDigest: z.string(),
|
|
197
|
+
planDigest: z.string(),
|
|
198
|
+
changed: z.boolean(),
|
|
199
|
+
json: z.unknown(),
|
|
200
|
+
changeSet: z.object({ member: z.object({ member: z.string(), planDigest: z.string().nullable() }).passthrough(), entries: z.array(z.unknown()) }),
|
|
201
|
+
planSetFile: z.string(),
|
|
202
|
+
root: z.string(),
|
|
203
|
+
}),
|
|
204
|
+
rootEntity,
|
|
205
|
+
);
|
|
206
|
+
|
|
184
207
|
/** One root of a pin-bump rollout ({@link PinRoot} in `../pin/waves.ts`). */
|
|
185
208
|
const pinRoot = z.strictObject({ root: z.string(), dependsOn: z.array(z.string()).optional(), tsSource: z.string().optional() });
|
|
186
209
|
|
package/src/op/builders.ts
CHANGED
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
ChoudoufuLiveLsArgs,
|
|
32
32
|
ChoudoufuLiveCheckArgs,
|
|
33
33
|
ChoudoufuAdoptArgs,
|
|
34
|
+
TerraformRootArgs,
|
|
34
35
|
} from "./activities/terraform";
|
|
35
36
|
import type { TerraformPinRolloutArgs } from "./activities/pin-rollout";
|
|
36
37
|
|
|
@@ -186,6 +187,19 @@ export const terraformShow = (
|
|
|
186
187
|
);
|
|
187
188
|
};
|
|
188
189
|
|
|
190
|
+
/**
|
|
191
|
+
* `choudoufuPlanSet` (#4483): `live-plan-set -json` over one live choudoufu
|
|
192
|
+
* root. Hand `step.out.changeSet` from each root's step to `composeChangeSet`
|
|
193
|
+
* to make one change-set document over several roots, such as one per cloud.
|
|
194
|
+
*/
|
|
195
|
+
export const choudoufuPlanSet = (
|
|
196
|
+
root: string,
|
|
197
|
+
opts?: WithStepRefs<Omit<TerraformRootArgs, "root">> & StepOpts,
|
|
198
|
+
): NamedActivityStep => {
|
|
199
|
+
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
|
|
200
|
+
return activity("choudoufuPlanSet", { root, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
|
|
201
|
+
};
|
|
202
|
+
|
|
189
203
|
/**
|
|
190
204
|
* One run of a pin-bump rollout (#3189). `opts` is
|
|
191
205
|
* {@link TerraformPinRolloutArgs} itself. It names no root: the rollout finds
|
package/src/pin/bundle.test.ts
CHANGED
|
@@ -38,6 +38,8 @@ describe("the pin bundle", () => {
|
|
|
38
38
|
expect(inputs.filter((i) => pattern.test(i)), String(pattern)).toEqual([]);
|
|
39
39
|
}
|
|
40
40
|
expect(inputs.map((i) => i.replace(/^.*?(packages|lexicons)\//, "$1/")).sort()).toEqual([
|
|
41
|
+
// The one config-file lister (chant#4457): node built-ins only, kept apart from the HCL parser.
|
|
42
|
+
"lexicons/terraform/src/hcl/files.ts",
|
|
41
43
|
"lexicons/terraform/src/pin/edit.ts",
|
|
42
44
|
"lexicons/terraform/src/pin/forge.ts",
|
|
43
45
|
"lexicons/terraform/src/pin/index.ts",
|
package/src/pin/edit.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import type { Hcl2Json } from "@intentius/chant/terraform/hcl2json";
|
|
26
26
|
import { movePin, type PinMove } from "./source";
|
|
27
|
+
import { isJsonConfigFile, jsonConfigTree } from "../hcl/files";
|
|
27
28
|
import { escapeHcl, scanBlocks, unescapeHcl, type ScannedAttribute, type ScannedBlock } from "./scan";
|
|
28
29
|
|
|
29
30
|
/** What to move: a module, named by its source without the pin, from one pin to another. */
|
|
@@ -99,6 +100,7 @@ interface Splice {
|
|
|
99
100
|
* is reported with its reason and left as it was.
|
|
100
101
|
*/
|
|
101
102
|
export async function editPins(text: string, file: string, request: PinRequest, parser: Hcl2Json): Promise<PinEditResult> {
|
|
103
|
+
if (isJsonConfigFile(file)) return jsonPins(text, file, request);
|
|
102
104
|
const terragrunt = isTerragruntFile(file);
|
|
103
105
|
const tree = (await parser.parse(file, text)) as Tree;
|
|
104
106
|
const blocks = scanBlocks(text, file);
|
|
@@ -155,3 +157,31 @@ export async function editPins(text: string, file: string, request: PinRequest,
|
|
|
155
157
|
function rewriteLiteral(raw: string, was: string, value: string): string {
|
|
156
158
|
return raw === was ? value : escapeHcl(value);
|
|
157
159
|
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The calls of the module in a `.tf.json` or `.tofu.json` file (chant#4457).
|
|
163
|
+
* The pin edit splices HCL text and has no splice for JSON syntax, so a call
|
|
164
|
+
* that would move is refused by name and the file is left as it is; a call
|
|
165
|
+
* already at the new pin, or at another one, reads as it does in HCL.
|
|
166
|
+
*/
|
|
167
|
+
function jsonPins(text: string, file: string, request: PinRequest): PinEditResult {
|
|
168
|
+
const calls: PinCallResult[] = [];
|
|
169
|
+
let tree: Record<string, unknown>;
|
|
170
|
+
try {
|
|
171
|
+
tree = jsonConfigTree(text);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return { content: text, calls: [{ outcome: "refused", reason: `not JSON the binary reads: ${err instanceof Error ? err.message : String(err)}`, file, call: "(file)" }] };
|
|
174
|
+
}
|
|
175
|
+
const modules = (tree.module && typeof tree.module === "object" ? tree.module : {}) as Record<string, unknown>;
|
|
176
|
+
for (const [name, bodies] of Object.entries(modules)) {
|
|
177
|
+
for (const body of Array.isArray(bodies) ? (bodies as Record<string, unknown>[]) : []) {
|
|
178
|
+
if (typeof body.source !== "string") continue;
|
|
179
|
+
const move = movePin(body.source, typeof body.version === "string" ? body.version : undefined, request);
|
|
180
|
+
if (!move) continue;
|
|
181
|
+
const at = { file, call: `module.${name}` };
|
|
182
|
+
if (move.outcome === "moved") calls.push({ outcome: "refused", reason: "the call is in Terraform's JSON syntax, which the pin edit does not write; move its pin by hand", ...at });
|
|
183
|
+
else calls.push({ ...move, ...at });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { content: text, calls };
|
|
187
|
+
}
|
package/src/pin/rollout.ts
CHANGED
|
@@ -48,6 +48,7 @@ import { editPins, type PinCallResult, type PinEditResult, type PinRequest } fro
|
|
|
48
48
|
import { checkPinRequest, moduleOf } from "./source";
|
|
49
49
|
import { planPinWaves, restrictWaves, terragruntDependencies, type PinRoot, type PinWave } from "./waves";
|
|
50
50
|
import { ghPinForge, type PinForge } from "./forge";
|
|
51
|
+
import { isConfigFileFor, listConfigFilesSafe } from "../hcl/files";
|
|
51
52
|
|
|
52
53
|
const execFileAsync = /* @__PURE__ */ promisify(execFile);
|
|
53
54
|
|
|
@@ -83,6 +84,8 @@ export interface PinRolloutOptions extends PinRequest {
|
|
|
83
84
|
editTs?: TsPinEditor;
|
|
84
85
|
/** Markdown appended to a wave's PR body, such as the grouped plan summary of #3188 once a caller has the wave's plans. */
|
|
85
86
|
planSummary?: (wave: PinWave) => Promise<string | undefined>;
|
|
87
|
+
/** The binary the roots run: `tofu` or `choudoufu` reads `.tofu` files too (chant#4457). Default `terraform`. */
|
|
88
|
+
binary?: string;
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
/** Where one root's pin stood on the base branch. */
|
|
@@ -169,22 +172,24 @@ export function pinWaveBranch(request: PinRequest, wave: number): string {
|
|
|
169
172
|
return `chant/pin/${slug(request.module)}/${slug(request.to)}/wave-${wave}`;
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
/**
|
|
173
|
-
|
|
175
|
+
/**
|
|
176
|
+
* The files the pin edit reads in a root directory: the configuration files
|
|
177
|
+
* the root's binary reads (the lexicon's one lister, chant#4457) and its
|
|
178
|
+
* `terragrunt.hcl`.
|
|
179
|
+
*/
|
|
180
|
+
function rootFiles(worktree: string, root: string, binary?: string): string[] {
|
|
174
181
|
const dir = join(worktree, root);
|
|
175
182
|
if (!existsSync(dir)) return [];
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.sort()
|
|
179
|
-
.map((f) => posix.join(root, f));
|
|
183
|
+
const names = [...listConfigFilesSafe(dir, binary), ...(existsSync(join(dir, "terragrunt.hcl")) ? ["terragrunt.hcl"] : [])];
|
|
184
|
+
return names.sort().map((f) => posix.join(root, f));
|
|
180
185
|
}
|
|
181
186
|
|
|
182
|
-
/** Every directory under the worktree with a
|
|
183
|
-
function candidateRoots(worktree: string): string[] {
|
|
187
|
+
/** Every directory under the worktree with a configuration file the binary reads, or a `terragrunt.hcl`. */
|
|
188
|
+
function candidateRoots(worktree: string, binary?: string): string[] {
|
|
184
189
|
const out: string[] = [];
|
|
185
190
|
const walk = (rel: string) => {
|
|
186
191
|
const entries = readdirSync(join(worktree, rel), { withFileTypes: true });
|
|
187
|
-
if (entries.some((e) => e.isFile() && (e.name
|
|
192
|
+
if (entries.some((e) => e.isFile() && (isConfigFileFor(e.name, binary) || e.name === "terragrunt.hcl"))) out.push(rel || ".");
|
|
188
193
|
for (const e of entries) if (e.isDirectory() && !SKIP_DIRS.has(e.name)) walk(rel ? posix.join(rel, e.name) : e.name);
|
|
189
194
|
};
|
|
190
195
|
walk("");
|
|
@@ -201,7 +206,7 @@ async function editRoot(worktree: string, root: PinRoot, request: PinRequest, op
|
|
|
201
206
|
return { state: { root: root.root, state: "refused", files, calls, reason: `generated from ${root.tsSource}, and this caller has no TypeScript editor` }, edits };
|
|
202
207
|
}
|
|
203
208
|
} else {
|
|
204
|
-
files = rootFiles(worktree, root.root);
|
|
209
|
+
files = rootFiles(worktree, root.root, options.binary);
|
|
205
210
|
}
|
|
206
211
|
for (const file of files) {
|
|
207
212
|
const path = join(worktree, file);
|
|
@@ -302,7 +307,7 @@ export async function runPinRollout(options: PinRolloutOptions): Promise<PinRoll
|
|
|
302
307
|
await run("git", ["worktree", "add", "--quiet", "--detach", worktree, baseRef], repo);
|
|
303
308
|
try {
|
|
304
309
|
// 1. Every root's pin on the base branch.
|
|
305
|
-
const declared = (options.roots ?? candidateRoots(worktree).map((root): PinRoot => ({ root }))).map((r) => ({
|
|
310
|
+
const declared = (options.roots ?? candidateRoots(worktree, options.binary).map((root): PinRoot => ({ root }))).map((r) => ({
|
|
306
311
|
...r,
|
|
307
312
|
root: dirOf(r.root),
|
|
308
313
|
...(r.dependsOn ? { dependsOn: r.dependsOn.map(dirOf) } : {}),
|