@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
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composed run of choudoufu estates (chant#4445, epic #4419): when every
|
|
3
|
+
* member runs choudoufu against a `live` estate (`Composition.estate`), one
|
|
4
|
+
* `choudoufu plan` and one `choudoufu apply` from a composed root of `module`
|
|
5
|
+
* calls (choudoufu#2059).
|
|
6
|
+
*
|
|
7
|
+
* An estate has no state file, so nothing is pulled, locked, split or
|
|
8
|
+
* written back: each resource's record goes to its own estate as it applies,
|
|
9
|
+
* and a `terraform_estate_outputs` read of another estate in the run is
|
|
10
|
+
* answered inside the run. The composed root is `writeEstateRoot`'s
|
|
11
|
+
* (./root.ts, #4441). The run takes the job's environment and assumes no
|
|
12
|
+
* role of its own: each estate's provider blocks name its role.
|
|
13
|
+
*
|
|
14
|
+
* choudoufu refuses a bare `show` under a `live` block, so the values the
|
|
15
|
+
* plan left unknown are read after the apply from a second plan of the
|
|
16
|
+
* composed root: its `prior_state`, sliced per module (./slice.ts, #4442) and
|
|
17
|
+
* read through `../applied-state`, the reader `chant terraform report` uses.
|
|
18
|
+
* The same plan says how far a failed apply got: a member whose part plans
|
|
19
|
+
* no change any more applied.
|
|
20
|
+
*
|
|
21
|
+
* Lifted from terragucci's `packages/terragucci/src/compose.ts` at c493e0e
|
|
22
|
+
* (`ComposedRun` with `estate` set: `plan`, `apply`, `finishEstate`,
|
|
23
|
+
* `showEstateMembers`, `composedEnv`). The state-backed run is #4444; this one
|
|
24
|
+
* has the same `plan()`, `apply()` and `release()` and returns the same
|
|
25
|
+
* shapes, and #4447 picks one by `Composition.estate`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { existsSync, mkdtempSync, readdirSync, rmdirSync, rmSync } from "node:fs";
|
|
29
|
+
import { tmpdir } from "node:os";
|
|
30
|
+
import { join, resolve } from "node:path";
|
|
31
|
+
import type { CapabilityGroupOutcome, CapabilityGroupPlan, GroupMemberOutcome } from "@intentius/chant/components/capability";
|
|
32
|
+
import type { ComposeFallback, Composition } from "../compose";
|
|
33
|
+
import type { TerraformConfig } from "../config";
|
|
34
|
+
import { stateOutputs, stateResources, type StateResourceValues } from "../applied-state";
|
|
35
|
+
import { terraformPlanDigest } from "../plan-digest";
|
|
36
|
+
import { terraformPlannedOutputs } from "../components/capability-plugin";
|
|
37
|
+
import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
|
|
38
|
+
import { isOlderVersion, MIN_CHOUDOUFU_ESTATES_VERSION, parseChoudoufuVersion } from "../op/activities/terraform";
|
|
39
|
+
import { runBinary, type BinaryExec } from "../state/exec";
|
|
40
|
+
import { memberApply, type ApplyEvents, type MemberApply } from "./apply-events";
|
|
41
|
+
import { liveEstate } from "./hcl";
|
|
42
|
+
import { stripModule, topModule } from "./module-address";
|
|
43
|
+
import { COMPOSE_DIR, withoutVarFiles, writeEstateRoot, type ComposedRoot } from "./root";
|
|
44
|
+
import { composedMemberPlans, planCounts, slicePlan, sliceText, type ComposedMemberPlan, type PlanCounts } from "./slice";
|
|
45
|
+
|
|
46
|
+
/** What a composed run's `plan()` answers. Assignable to core's `CapabilityGroupPlan` (ws-107). */
|
|
47
|
+
export interface ComposedRunPlan extends CapabilityGroupPlan {
|
|
48
|
+
/** Each member's part of the plan, by component. Empty when the run fell back or the plan failed. */
|
|
49
|
+
members: Record<string, ComposedMemberPlan>;
|
|
50
|
+
/** What the approval binds beside the members' own plans: the composed plan and the generated files. Empty when nothing planned. */
|
|
51
|
+
digest: string;
|
|
52
|
+
/** Why the run did not plan as one: the members plan wave by wave instead. Decided before anything planned. */
|
|
53
|
+
fallback?: ComposeFallback;
|
|
54
|
+
/** Why the composed plan failed: every member is plan-failed. */
|
|
55
|
+
error?: string;
|
|
56
|
+
/** The member {@link error} is about, when it is about one (a hook, an init, a lock). */
|
|
57
|
+
member?: string;
|
|
58
|
+
/** The composed plan's `show -json`. */
|
|
59
|
+
plan?: unknown;
|
|
60
|
+
/** The saved plan file `apply()` applies. */
|
|
61
|
+
planFile: string;
|
|
62
|
+
/** The composed plan's `show` text. */
|
|
63
|
+
text: string;
|
|
64
|
+
/** Each member's part of the `show` text, by component. */
|
|
65
|
+
texts: Record<string, string>;
|
|
66
|
+
/** sha256 of each generated file of the composed root, as `ComposedRoot.generated`. */
|
|
67
|
+
generated: [string, string][];
|
|
68
|
+
/** The whole run's counts. */
|
|
69
|
+
counts: PlanCounts;
|
|
70
|
+
/** The binary's `Plan:` line for the whole run. */
|
|
71
|
+
summary: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One member's outcome of a composed apply. Assignable to core's `GroupMemberOutcome` (ws-107). */
|
|
75
|
+
export interface ComposedMemberOutcome extends GroupMemberOutcome {
|
|
76
|
+
status: MemberApply;
|
|
77
|
+
/** Its planned changes. */
|
|
78
|
+
planned: number;
|
|
79
|
+
/** Its planned changes that completed. */
|
|
80
|
+
done: number;
|
|
81
|
+
/** Each resource of its part of the state after the apply, by address with its module taken off. */
|
|
82
|
+
resources?: Map<string, StateResourceValues>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** What a composed run's `apply()` answers. Assignable to core's `CapabilityGroupOutcome` (ws-107). */
|
|
86
|
+
export interface ComposedRunApply extends CapabilityGroupOutcome {
|
|
87
|
+
/** Every member applied or had no changes, and the binary exited 0. */
|
|
88
|
+
ok: boolean;
|
|
89
|
+
members: Record<string, ComposedMemberOutcome>;
|
|
90
|
+
/** `show -json` of the plan made after the apply. Absent when it could not be made. */
|
|
91
|
+
after?: unknown;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The surface #4447 drives, whichever run `Composition.estate` picks. */
|
|
95
|
+
export interface ComposedRunner {
|
|
96
|
+
plan(): Promise<ComposedRunPlan>;
|
|
97
|
+
apply(): Promise<ComposedRunApply>;
|
|
98
|
+
release(): Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface EstateRunOptions {
|
|
102
|
+
/** The checkout's root: where `chant.config.*` was loaded from. `.chant-compose/` is written here. */
|
|
103
|
+
projectRoot: string;
|
|
104
|
+
/** What `compositionFor` answered, with `estate` set and two or more members. */
|
|
105
|
+
composition: Composition;
|
|
106
|
+
/** The config's `terraform` namespace: a root's `estate`. */
|
|
107
|
+
terraform: TerraformConfig;
|
|
108
|
+
/** The job's environment. The run adds no role to it. */
|
|
109
|
+
env: NodeJS.ProcessEnv;
|
|
110
|
+
/** The choudoufu binary, by name or path. Default `terraform.binary`, else `choudoufu`. */
|
|
111
|
+
binary?: string;
|
|
112
|
+
/** Where the plan files go. Default: a new directory under the system's temp directory. */
|
|
113
|
+
work?: string;
|
|
114
|
+
/** How the binary runs. Default {@link runBinary}; a test injects a fake. */
|
|
115
|
+
exec?: BinaryExec;
|
|
116
|
+
log?: (line: string) => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const firstLines = (s: string, n = 20): string => s.trim().split("\n").slice(-n).join("\n");
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Why a choudoufu's `version` output cannot run a composed run of estates, or
|
|
123
|
+
* undefined when it can. A release older than
|
|
124
|
+
* {@link MIN_CHOUDOUFU_ESTATES_VERSION} falls back; a dev build names no
|
|
125
|
+
* release and passes.
|
|
126
|
+
*/
|
|
127
|
+
export function estateVersionFallback(versionOutput: string): ComposeFallback | undefined {
|
|
128
|
+
const version = parseChoudoufuVersion(versionOutput);
|
|
129
|
+
if (version === undefined || !isOlderVersion(version, MIN_CHOUDOUFU_ESTATES_VERSION)) return undefined;
|
|
130
|
+
return {
|
|
131
|
+
kind: "binary",
|
|
132
|
+
reason: `choudoufu ${version} is older than v${MIN_CHOUDOUFU_ESTATES_VERSION}, the first release that plans and applies several estates as one (choudoufu#2059)`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* What choudoufu's human `apply` output says of each module: the changes
|
|
138
|
+
* that completed (`<addr>: Creation complete`, `Modifications complete`,
|
|
139
|
+
* `Destruction complete`), started (`Creating...` and the rest) or errored
|
|
140
|
+
* (an `Error:` box naming `with <addr>,`). An estate's apply runs under live
|
|
141
|
+
* resource markers and prints no `-json` lines, so this reads the text.
|
|
142
|
+
*/
|
|
143
|
+
export function readEstateApplyText(out: string, modules: readonly string[]): ApplyEvents {
|
|
144
|
+
const ev: ApplyEvents = { done: new Map(), started: new Map(), errored: new Map(), other: [] };
|
|
145
|
+
const known = new Set(modules);
|
|
146
|
+
const moduleOf = (addr: string): string | undefined => {
|
|
147
|
+
const m = topModule(addr);
|
|
148
|
+
return m !== undefined && known.has(m) && addr !== `module.${m}` ? m : undefined;
|
|
149
|
+
};
|
|
150
|
+
const bump = (map: Map<string, number>, m: string): void => {
|
|
151
|
+
map.set(m, (map.get(m) ?? 0) + 1);
|
|
152
|
+
};
|
|
153
|
+
const lines = out.split("\n").map((l) => l.replace(/^[│╷╵\s]+/, "").trimEnd());
|
|
154
|
+
for (let i = 0; i < lines.length; i++) {
|
|
155
|
+
const l = lines[i]!;
|
|
156
|
+
const hit = l.match(/^(module\.\S+): (Creation|Modifications|Destruction) complete\b/);
|
|
157
|
+
if (hit) {
|
|
158
|
+
const m = moduleOf(hit[1]!);
|
|
159
|
+
if (m) bump(ev.done, m);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const start = l.match(/^(module\.\S+): (Creating|Modifying|Destroying)\.\.\./);
|
|
163
|
+
if (start) {
|
|
164
|
+
const m = moduleOf(start[1]!);
|
|
165
|
+
if (m) bump(ev.started, m);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (l.startsWith("Error: ")) {
|
|
169
|
+
let addr: string | undefined;
|
|
170
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 8) && !lines[j]!.startsWith("Error: "); j++) {
|
|
171
|
+
const w = lines[j]!.match(/^with (module\.[^,\s]+),?$/);
|
|
172
|
+
if (w) {
|
|
173
|
+
addr = w[1]!;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const m = addr ? moduleOf(addr) : undefined;
|
|
178
|
+
if (m && !ev.errored.has(m)) ev.errored.set(m, `${stripModule(addr!)}: ${l}`);
|
|
179
|
+
else if (!m) ev.other.push(l);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return ev;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Each member's outcome from its planned changes, what the apply printed, its
|
|
187
|
+
* exit code and, when the post-apply plan was made, the changes each member's
|
|
188
|
+
* part still plans. A clean exit means every planned change applied. After a
|
|
189
|
+
* failure, a member's changes left in the post-apply plan say how many of its
|
|
190
|
+
* planned changes applied; without that plan the printed completions do.
|
|
191
|
+
*/
|
|
192
|
+
export function estateOutcomes(
|
|
193
|
+
c: Pick<Composition, "order" | "modules">,
|
|
194
|
+
planned: ReadonlyMap<string, number>,
|
|
195
|
+
out: string,
|
|
196
|
+
code: number,
|
|
197
|
+
remaining?: ReadonlyMap<string, number>,
|
|
198
|
+
): { members: Record<string, { status: MemberApply; done: number; planned: number; error?: string }>; other: string[] } {
|
|
199
|
+
const ev = readEstateApplyText(out, c.order.map((member) => c.modules.get(member)!));
|
|
200
|
+
for (const member of c.order) {
|
|
201
|
+
const m = c.modules.get(member)!;
|
|
202
|
+
const n = planned.get(member) ?? 0;
|
|
203
|
+
if (code === 0) ev.done.set(m, n);
|
|
204
|
+
else if (remaining?.has(member)) ev.done.set(m, Math.max(0, n - remaining.get(member)!));
|
|
205
|
+
}
|
|
206
|
+
const members: Record<string, { status: MemberApply; done: number; planned: number; error?: string }> = {};
|
|
207
|
+
for (const member of c.order) {
|
|
208
|
+
const m = c.modules.get(member)!;
|
|
209
|
+
const n = planned.get(member) ?? 0;
|
|
210
|
+
const { apply, done } = memberApply(m, n, ev);
|
|
211
|
+
const error = ev.errored.get(m);
|
|
212
|
+
members[member] = { status: apply, done, planned: n, ...(error && apply !== "applied" && apply !== "no-changes" ? { error } : {}) };
|
|
213
|
+
}
|
|
214
|
+
return { members, other: ev.other };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* One composed run of estates: `plan()`, then `apply()` once the approval is
|
|
219
|
+
* given, then `release()` whatever happened. No call reads, locks or writes
|
|
220
|
+
* a state.
|
|
221
|
+
*/
|
|
222
|
+
export class EstateRun implements ComposedRunner {
|
|
223
|
+
private readonly exec: BinaryExec;
|
|
224
|
+
private readonly binary: string;
|
|
225
|
+
private readonly composeDir: string;
|
|
226
|
+
private readonly work: string;
|
|
227
|
+
private readonly madeWork: boolean;
|
|
228
|
+
private planned?: ComposedRunPlan;
|
|
229
|
+
private root?: ComposedRoot;
|
|
230
|
+
|
|
231
|
+
constructor(private readonly o: EstateRunOptions) {
|
|
232
|
+
const c = o.composition;
|
|
233
|
+
if (!c.estate || c.fallback || c.members.length < 2) throw new Error(`EstateRun runs a composition of estates with two or more members${c.fallback ? `; this one fell back: ${c.fallback.reason}` : ""}`);
|
|
234
|
+
this.exec = o.exec ?? runBinary;
|
|
235
|
+
this.binary = o.binary ?? o.terraform.binary ?? "choudoufu";
|
|
236
|
+
this.composeDir = join(resolve(o.projectRoot), COMPOSE_DIR);
|
|
237
|
+
this.madeWork = o.work === undefined;
|
|
238
|
+
this.work = o.work ?? mkdtempSync(join(tmpdir(), "chant-compose-estate-"));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private get c(): Composition {
|
|
242
|
+
return this.o.composition;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private log(line: string): void {
|
|
246
|
+
this.o.log?.(line);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The composed root's environment: the job's, with no role added (each
|
|
251
|
+
* estate's provider blocks name its role) and its `-var-file` flags taken
|
|
252
|
+
* out. With a shared provider cache and no lock file to check a cached
|
|
253
|
+
* provider against, the cache may fill the lock file.
|
|
254
|
+
*/
|
|
255
|
+
private env(): NodeJS.ProcessEnv {
|
|
256
|
+
const env = withoutVarFiles(this.o.env);
|
|
257
|
+
return env.TF_PLUGIN_CACHE_DIR && !existsSync(join(this.composeDir, ".terraform.lock.hcl")) ? { ...env, TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE: "true" } : env;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The estate each member's records go to: its root's `estate`, else its `live` block's. */
|
|
261
|
+
private estateOf(member: string): string | undefined {
|
|
262
|
+
const key = this.c.roots.get(member);
|
|
263
|
+
return (key !== undefined ? this.o.terraform.roots[key]?.estate : undefined) ?? liveEstate(this.c.dirs.get(member)!, this.binary).estate;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private notPlanned(extra: { fallback?: ComposeFallback; error?: string }): ComposedRunPlan {
|
|
267
|
+
return {
|
|
268
|
+
members: {},
|
|
269
|
+
digest: "",
|
|
270
|
+
planFile: join(this.work, "composed.tfplan"),
|
|
271
|
+
text: "",
|
|
272
|
+
texts: {},
|
|
273
|
+
generated: [],
|
|
274
|
+
counts: { add: 0, change: 0, destroy: 0, changes: 0, destroys: 0 },
|
|
275
|
+
summary: "",
|
|
276
|
+
...extra,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Why this choudoufu cannot run the composition, or undefined. Runs
|
|
282
|
+
* `choudoufu version`; what #4447's `groupFallback` asks before planning.
|
|
283
|
+
*/
|
|
284
|
+
async fallback(): Promise<ComposeFallback | undefined> {
|
|
285
|
+
const v = await this.exec(this.binary, ["version"], resolve(this.o.projectRoot), this.o.env);
|
|
286
|
+
// A binary that cannot say its version is left to the plan, which says why it fails.
|
|
287
|
+
return v.code === 0 ? estateVersionFallback(v.stdout) : undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private writeRoot(): ComposedRoot {
|
|
291
|
+
this.root = writeEstateRoot({ projectRoot: this.o.projectRoot, composition: this.c });
|
|
292
|
+
return this.root;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private restoreRoot(): void {
|
|
296
|
+
const r = this.root;
|
|
297
|
+
this.root = undefined;
|
|
298
|
+
r?.restore();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Check the binary's version, write the composed root, init it and plan
|
|
303
|
+
* once, then cut the plan into each member's part. The composed root is
|
|
304
|
+
* taken away again before this returns.
|
|
305
|
+
*/
|
|
306
|
+
async plan(): Promise<ComposedRunPlan> {
|
|
307
|
+
const fallback = await this.fallback();
|
|
308
|
+
if (fallback) {
|
|
309
|
+
this.log(`composed: ${fallback.reason}; the estates plan wave by wave`);
|
|
310
|
+
return this.notPlanned({ fallback });
|
|
311
|
+
}
|
|
312
|
+
let root: ComposedRoot;
|
|
313
|
+
try {
|
|
314
|
+
root = this.writeRoot();
|
|
315
|
+
} catch (e) {
|
|
316
|
+
return this.notPlanned({ error: `the composed root could not be written: ${(e as Error).message}` });
|
|
317
|
+
}
|
|
318
|
+
const planFile = join(this.work, "composed.tfplan");
|
|
319
|
+
let plan: unknown;
|
|
320
|
+
let text = "";
|
|
321
|
+
let summary = "";
|
|
322
|
+
try {
|
|
323
|
+
const env = this.env();
|
|
324
|
+
const init = await this.exec(this.binary, ["init", "-input=false", "-no-color"], this.composeDir, env);
|
|
325
|
+
if (init.code !== 0) return this.notPlanned({ error: `the composed root's init failed\n${firstLines(init.out)}` });
|
|
326
|
+
const p = await this.exec(this.binary, ["plan", "-input=false", "-no-color", `-out=${planFile}`], this.composeDir, env);
|
|
327
|
+
if (p.code !== 0) return this.notPlanned({ error: `the composed plan failed\n${firstLines(p.out)}` });
|
|
328
|
+
summary = p.stdout.match(/Plan: .*|No changes\..*/)?.[0] ?? "planned";
|
|
329
|
+
const json = await this.exec(this.binary, ["show", "-json", planFile], this.composeDir, env);
|
|
330
|
+
try {
|
|
331
|
+
plan = JSON.parse(json.stdout);
|
|
332
|
+
} catch {
|
|
333
|
+
return this.notPlanned({ error: `show -json printed no plan\n${firstLines(json.out, 6)}` });
|
|
334
|
+
}
|
|
335
|
+
text = (await this.exec(this.binary, ["show", "-no-color", planFile], this.composeDir, env)).stdout;
|
|
336
|
+
} finally {
|
|
337
|
+
this.restoreRoot();
|
|
338
|
+
}
|
|
339
|
+
const members = composedMemberPlans(this.c, plan, { planner: "choudoufu", scope: (m) => this.estateOf(m) });
|
|
340
|
+
const texts = Object.fromEntries(this.c.order.map((m) => [m, sliceText(text, this.c.modules.get(m)!, members[m]!.counts)] as const));
|
|
341
|
+
const digest = computePlanDigest("terraform-compose-estate", { plan: terraformPlanDigest(plan), modules: this.c.order.map((m) => [m, this.c.modules.get(m)]), generated: root.generated });
|
|
342
|
+
this.planned = { members, digest, plan, planFile, text, texts, generated: root.generated, counts: planCounts(plan), summary };
|
|
343
|
+
return this.planned;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Apply the saved plan from the composed root, then plan again and read
|
|
348
|
+
* each member's values from that plan's `prior_state`. A failed apply
|
|
349
|
+
* leaves what applied applied, and each member's outcome says how far its
|
|
350
|
+
* part got.
|
|
351
|
+
*/
|
|
352
|
+
async apply(): Promise<ComposedRunApply> {
|
|
353
|
+
const p = this.planned;
|
|
354
|
+
const notStarted = (error: string): ComposedRunApply => ({
|
|
355
|
+
ok: false,
|
|
356
|
+
members: Object.fromEntries(this.c.order.map((m) => [m, { status: "not-started" as const, planned: p?.members[m]?.counts.changes ?? 0, done: 0 }] as const)),
|
|
357
|
+
error,
|
|
358
|
+
});
|
|
359
|
+
if (!p?.plan) return notStarted("the composed run did not plan");
|
|
360
|
+
let root: ComposedRoot;
|
|
361
|
+
try {
|
|
362
|
+
root = this.writeRoot();
|
|
363
|
+
} catch (e) {
|
|
364
|
+
return notStarted(`the composed root could not be written: ${(e as Error).message}`);
|
|
365
|
+
}
|
|
366
|
+
let out = "";
|
|
367
|
+
let code = 1;
|
|
368
|
+
let after: unknown;
|
|
369
|
+
try {
|
|
370
|
+
if (JSON.stringify(root.generated) !== JSON.stringify(p.generated)) return notStarted("the composed root differs from the one planned, so nothing applied");
|
|
371
|
+
const a = await this.exec(this.binary, ["apply", "-input=false", "-no-color", p.planFile], this.composeDir, this.env());
|
|
372
|
+
out = a.out;
|
|
373
|
+
code = a.code;
|
|
374
|
+
for (const line of a.stdout.split("\n")) if (line.trim()) this.log(`composed: ${line}`);
|
|
375
|
+
after = await this.planAfter();
|
|
376
|
+
} finally {
|
|
377
|
+
this.restoreRoot();
|
|
378
|
+
}
|
|
379
|
+
const planned = new Map(this.c.order.map((m) => [m, p.members[m]!.counts.changes] as const));
|
|
380
|
+
const slices = after !== undefined ? new Map(this.c.order.map((m) => [m, slicePlan(after, this.c.modules.get(m)!)] as const)) : undefined;
|
|
381
|
+
const remaining = slices ? new Map(this.c.order.map((m) => [m, planCounts(slices.get(m)).changes] as const)) : undefined;
|
|
382
|
+
const ended = estateOutcomes(this.c, planned, out, code, remaining);
|
|
383
|
+
const members: Record<string, ComposedMemberOutcome> = {};
|
|
384
|
+
for (const m of this.c.order) {
|
|
385
|
+
const row: ComposedMemberOutcome = { ...ended.members[m]! };
|
|
386
|
+
const slice = slices?.get(m);
|
|
387
|
+
if (slice !== undefined) {
|
|
388
|
+
row.resources = stateResources(slice);
|
|
389
|
+
if (row.status === "applied" || row.status === "no-changes") {
|
|
390
|
+
// A state choudoufu builds from records may carry no root outputs; the same plan's planned outputs, all known after the apply, stand in.
|
|
391
|
+
const outputs = stateOutputs(slice);
|
|
392
|
+
row.outputs = Object.keys(outputs).length > 0 ? outputs : terraformPlannedOutputs(slice).known;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
members[m] = row;
|
|
396
|
+
}
|
|
397
|
+
const ok = code === 0 && Object.values(members).every((x) => x.status === "applied" || x.status === "no-changes");
|
|
398
|
+
const error = ok ? undefined : (ended.other[0] ?? Object.values(members).find((x) => x.error)?.error ?? "the composed apply failed");
|
|
399
|
+
return { ok, members, ...(after !== undefined ? { after } : {}), ...(error ? { error } : {}) };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* `show -json` of a plan of the composed root made after the apply, whose
|
|
404
|
+
* `prior_state` choudoufu builds from the records and the live system.
|
|
405
|
+
* Undefined when it cannot be made: the values unknown at plan then stay
|
|
406
|
+
* unknown. Never fails the run.
|
|
407
|
+
*/
|
|
408
|
+
private async planAfter(): Promise<unknown> {
|
|
409
|
+
const planFile = join(this.work, "composed-after.tfplan");
|
|
410
|
+
const env = this.env();
|
|
411
|
+
try {
|
|
412
|
+
const p = await this.exec(this.binary, ["plan", "-input=false", "-no-color", `-out=${planFile}`], this.composeDir, env);
|
|
413
|
+
if (p.code !== 0) {
|
|
414
|
+
this.log(`composed: the estates could not be planned after the apply; the values unknown at plan stay unknown\n${firstLines(p.out)}`);
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
const s = await this.exec(this.binary, ["show", "-json", planFile], this.composeDir, env);
|
|
418
|
+
if (s.code !== 0) return undefined;
|
|
419
|
+
return JSON.parse(s.stdout) as unknown;
|
|
420
|
+
} catch (e) {
|
|
421
|
+
this.log(`composed: the estates could not be read after the apply (${(e as Error).message}); the values unknown at plan stay unknown`);
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Take the composed root away if a call left it, with the binary's working
|
|
428
|
+
* files under `.chant-compose/`, and the plan files. There is no lock to
|
|
429
|
+
* let go of. Safe to call more than once.
|
|
430
|
+
*/
|
|
431
|
+
async release(): Promise<void> {
|
|
432
|
+
this.restoreRoot();
|
|
433
|
+
for (const f of [".terraform", ".terraform.lock.hcl"]) rmSync(join(this.composeDir, f), { recursive: true, force: true });
|
|
434
|
+
try {
|
|
435
|
+
if (existsSync(this.composeDir) && readdirSync(this.composeDir).length === 0) rmdirSync(this.composeDir);
|
|
436
|
+
} catch {
|
|
437
|
+
// Gone already.
|
|
438
|
+
}
|
|
439
|
+
if (this.madeWork) rmSync(this.work, { recursive: true, force: true });
|
|
440
|
+
else for (const f of ["composed.tfplan", "composed-after.tfplan"]) rmSync(join(this.work, f), { force: true });
|
|
441
|
+
this.planned = undefined;
|
|
442
|
+
}
|
|
443
|
+
}
|